/*
 * File:        jquery.dataTables.js
 * Version:     1.7.0
 * CVS:         $Id$
 * Description: Paginate, search and sort HTML tables
 * Author:      Allan Jardine (www.sprymedia.co.uk)
 * Created:     28/3/2008
 * Modified:    $Date$ by $Author$
 * Language:    Javascript
 * License:     GPL v2 or BSD 3 point style
 * Project:     Mtaala
 * Contact:     allan.jardine@sprymedia.co.uk
 * 
 * Copyright 2008-2010 Allan Jardine, all rights reserved.
 *
 * This source file is free software, under either the GPL v2 license or a
 * BSD style license, as supplied with this software.
 * 
 * This source file is distributed in the hope that it will be useful, but 
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE. See the license files for details.
 * 
 * For details please refer to: http://www.datatables.net
 */

/*
 * When considering jsLint, we need to allow eval() as it it is used for reading cookies and  building the dynamic multi-column sort functions.
 在考虑JSlint时，我们需要允许eval（），因为它用于读取cookie和构建动态多列排序函数。
 */
/*jslint evil: true, undef: true, browser: true */
/*globals $, jQuery,
全局函数
_fnExternApiFunc, _fnInitalise, _fnLanguageProcess, _fnAddColumn, _fnColumnOptions,
_fnAddData, _fnGatherData, _fnDrawHead, _fnDraw, _fnReDraw, _fnAjaxUpdate, _fnAjaxUpdateDraw, _fnAddOptionsHtml,
_fnFeatureHtmlTable, _fnScrollDraw, _fnAjustColumnSizing, _fnFeatureHtmlFilter, _fnFilterComplete, _fnFilterCustom,
_fnFilterColumn, _fnFilter, _fnBuildSearchArray, _fnFilterCreateSearch, _fnDataToSearch, _fnSort, _fnSortAttachListener,
_fnSortingClasses, _fnFeatureHtmlPaginate, _fnPageChange, _fnFeatureHtmlInfo, _fnUpdateInfo, _fnFeatureHtmlLength, _fnFeatureHtmlProcessing,
_fnProcessingDisplay, _fnVisibleToColumnIndex, _fnColumnIndexToVisible, _fnNodeToDataIndex, _fnVisbleColumns, _fnCalculateEnd, _fnConvertToWidth,
_fnCalculateColumnWidths, _fnScrollingWidthAdjust, _fnGetWidestNode, _fnGetMaxLenString, _fnStringToCss, _fnArrayCmp, _fnDetectType,
_fnSettingsFromNode, _fnGetDataMaster, _fnGetTrNodes, _fnGetTdNodes, _fnEscapeRegex, _fnDeleteIndex, _fnReOrderIndex, _fnColumnOrdering,
_fnLog, _fnClearTable, _fnSaveState, _fnLoadState, _fnCreateCookie, _fnReadCookie, _fnGetUniqueThs, _fnScrollBarWidth, _fnApplyToChildren, _fnMap

  */

(function ($, window, document) {
	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 * Section - DataTables variables
	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

	/*
	 * Variable: dataTableSettings
	 * Purpose:  Store the settings for each dataTables instance 
	 *  存储每个数据表实例的设置
	 * Scope:    jQuery.fn
	 */
	$.fn.dataTableSettings = [];
	var _aoSettings = $.fn.dataTableSettings;
	/* Short reference for fast internal lookup 快速内部查找的简短参考 */

	/*
	 * Variable: dataTableExt
	 * Purpose:  Container for customisable parts of DataTables
	 *           数据表可自定义部分的容器
	 * Scope:    jQuery.fn
	 */
	$.fn.dataTableExt = {};
	var _oExt = $.fn.dataTableExt;

	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 * Section - DataTables extensible (可扩展的) objects
	 * 
	 * The _oExt object is used to provide an area where user dfined plugins can be 
	 * added to DataTables. The following properties of the object are used:
	 *   oApi - Plug-in API functions
	 *   aTypes - Auto-detection of types
	 *   oSort - Sorting functions used by DataTables (based on the type)
	 *   oPagination - Pagination functions for different input styles
	 * oext对象用于提供一个区域，在该区域中可以将用户数据链接插件添加到数据表中。
		使用对象的以下属性:
		OAPI-插件API函数
		aTypes-自动检测类型
		osort-数据表使用的排序函数（基于类型）
		oPagination-不同输入样式的分页功能
	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */


	// _oExt.sVersion
	// _oExt.sErrMode
	// _oExt.iApiIndex
	// _oExt.oApi
	// _oExt.afnFiltering
	// _oExt.aoFeatures
	// _oExt.ofnSearch
	// _oExt.afnSortData
	// _oExt.oStdClasses
	// _oExt.oJUIClasses
	// _oExt.oPagination
	// _oExt.oSort
	// _oExt.aTypes
	// _oExt.fnVersionCheck
	// _oExt._oExternConfig

	/*
	 * Variable: sVersion
	 * Purpose:  Version string for plug-ins to check compatibility
	 * Scope:    jQuery.fn.dataTableExt
	 * Notes:    Allowed format is a.b.c.d.e where:
	 *   a:int, b:int, c:int, d:string(dev|beta), e:int. d and e are optional
	 */
	_oExt.sVersion = "1.7.0";

	/*
	 * Variable: sErrMode
	 * Purpose:  How should DataTables report an error. Can take the value 'alert' or 'throw'
	 * Scope:    jQuery.fn.dataTableExt
	 */
	_oExt.sErrMode = "alert";

	/*
	 * Variable: iApiIndex
	 * Purpose:  Index for what 'this' index API functions should use
	 * 			“this”索引API函数应使用的索引
	 * Scope:    jQuery.fn.dataTableExt
	 */
	_oExt.iApiIndex = 0;

	/*
	 * Variable: oApi
	 * Purpose:  Container for plugin API functions
	 * Scope:    jQuery.fn.dataTableExt
	 */
	_oExt.oApi = {};

	/*
	 * Variable: aFiltering
	 * Purpose:  Container for plugin filtering functions
	 * Scope:    jQuery.fn.dataTableExt
	 */
	_oExt.afnFiltering = [];

	/*
	 * Variable: aoFeatures
	 * Purpose:  Container for plugin function functions
	 * Scope:    jQuery.fn.dataTableExt
	 * Notes:    Array of objects with the following parameters:
	 *   fnInit: Function for initialisation of Feature. Takes oSettings and returns node
	 *   cFeature: Character that will be matched in sDom - case sensitive
	 *   sFeature: Feature name - just for completeness :-)
	 */
	_oExt.aoFeatures = [];

	/*
	 * Variable: ofnSearch
	 * Purpose:  Container for custom filtering functions
	 * Scope:    jQuery.fn.dataTableExt
	 * Notes:    This is an object (the name should match the type) for custom filtering function,
	 *   which can be used for live DOM checking or formatted text filtering
	 * 这是自定义筛选函数的对象（名称应与类型匹配），可用于实时DOM检查或格式化文本筛选。
	 */
	_oExt.ofnSearch = {};

	/*
	 * Variable: afnSortData
	 * Purpose:  Container for custom sorting data source functions
	 * Scope:    jQuery.fn.dataTableExt
	 * Notes:    Array (associative) of functions which is run prior to a column of this 
	 *   'SortDataType' being sorted upon.
	 *   Function input parameters:
	 *     object:oSettings-  DataTables settings object
	 *     int:iColumn - Target column number
	 *   Return value: Array of data which exactly matched the full data set size for the column to
	 *     be sorted upon
	 */
	_oExt.afnSortData = [];

	/*
	 * Variable: oStdClasses
	 * Purpose:  Storage for the various classes that DataTables uses
	 * Scope:    jQuery.fn.dataTableExt
	 */
	_oExt.oStdClasses = {
		/* Two buttons buttons */
		"sPagePrevEnabled": "paginate_enabled_previous",
		"sPagePrevDisabled": "paginate_disabled_previous",
		"sPageNextEnabled": "paginate_enabled_next",
		"sPageNextDisabled": "paginate_disabled_next",
		"sPageJUINext": "",
		"sPageJUIPrev": "",

		/* Full numbers paging buttons */
		"sPageButton": "paginate_button",
		"sPageButtonActive": "paginate_active",
		"sPageButtonStaticDisabled": "paginate_button",
		"sPageFirst": "first",
		"sPagePrevious": "previous",
		"sPageNext": "next",
		"sPageLast": "last",

		/* Stripping classes */
		"sStripOdd": "odd",
		"sStripEven": "even",

		/* Empty row */
		"sRowEmpty": "dataTables_empty",

		/* Features */
		"sWrapper": "dataTables_wrapper",
		"sFilter": "dataTables_filter",
		"sInfo": "dataTables_info",
		"sPaging": "dataTables_paginate paging_", /* Note that the type is postfixed */
		"sLength": "dataTables_length",
		"sProcessing": "dataTables_processing",

		/* Sorting */
		"sSortAsc": "sorting_asc",
		"sSortDesc": "sorting_desc",
		"sSortable": "sorting", /* Sortable in both directions */
		"sSortableAsc": "sorting_asc_disabled",
		"sSortableDesc": "sorting_desc_disabled",
		"sSortableNone": "sorting_disabled",
		"sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */
		"sSortJUIAsc": "",
		"sSortJUIDesc": "",
		"sSortJUI": "",
		"sSortJUIAscAllowed": "",
		"sSortJUIDescAllowed": "",

		/* Scrolling */
		"sScrollWrapper": "dataTables_scroll",
		"sScrollHead": "dataTables_scrollHead",
		"sScrollHeadInner": "dataTables_scrollHeadInner",
		"sScrollBody": "dataTables_scrollBody",
		"sScrollFoot": "dataTables_scrollFoot",
		"sScrollFootInner": "dataTables_scrollFootInner",

		/* Misc */
		"sFooterTH": ""
	};

	/*
	 * Variable: oJUIClasses
	 * Purpose:  Storage for the various classes that DataTables uses - jQuery UI suitable
	 * Scope:    jQuery.fn.dataTableExt
	 */
	_oExt.oJUIClasses = {
		/* Two buttons buttons */
		"sPagePrevEnabled": "fg-button ui-state-default ui-corner-left",
		"sPagePrevDisabled": "fg-button ui-state-default ui-corner-left ui-state-disabled",
		"sPageNextEnabled": "fg-button ui-state-default ui-corner-right",
		"sPageNextDisabled": "fg-button ui-state-default ui-corner-right ui-state-disabled",
		"sPageJUINext": "ui-icon ui-icon-circle-arrow-e",
		"sPageJUIPrev": "ui-icon ui-icon-circle-arrow-w",

		/* Full numbers paging buttons */
		"sPageButton": "fg-button ui-state-default",
		"sPageButtonActive": "fg-button ui-state-default ui-state-disabled",
		"sPageButtonStaticDisabled": "fg-button ui-state-default ui-state-disabled",
		"sPageFirst": "first ui-corner-tl ui-corner-bl",
		"sPagePrevious": "previous",
		"sPageNext": "next",
		"sPageLast": "last ui-corner-tr ui-corner-br",

		/* Stripping classes */
		"sStripOdd": "odd",
		"sStripEven": "even",

		/* Empty row */
		"sRowEmpty": "dataTables_empty",

		/* Features */
		"sWrapper": "dataTables_wrapper",
		"sFilter": "dataTables_filter",
		"sInfo": "dataTables_info",
		"sPaging": "dataTables_paginate fg-buttonset fg-buttonset-multi paging_", /* Note that the type is postfixed */
		"sLength": "dataTables_length",
		"sProcessing": "dataTables_processing",

		/* Sorting */
		"sSortAsc": "ui-state-default",
		"sSortDesc": "ui-state-default",
		"sSortable": "ui-state-default",
		"sSortableAsc": "ui-state-default",
		"sSortableDesc": "ui-state-default",
		"sSortableNone": "ui-state-default",
		"sSortColumn": "sorting_", /* Note that an int is postfixed for the sorting order */
		"sSortJUIAsc": "css_right ui-icon ui-icon-triangle-1-n",
		"sSortJUIDesc": "css_right ui-icon ui-icon-triangle-1-s",
		"sSortJUI": "css_right ui-icon ui-icon-carat-2-n-s",
		"sSortJUIAscAllowed": "css_right ui-icon ui-icon-carat-1-n",
		"sSortJUIDescAllowed": "css_right ui-icon ui-icon-carat-1-s",

		/* Scrolling */
		"sScrollWrapper": "dataTables_scroll",
		"sScrollHead": "dataTables_scrollHead ui-state-default",
		"sScrollHeadInner": "dataTables_scrollHeadInner",
		"sScrollBody": "dataTables_scrollBody",
		"sScrollFoot": "dataTables_scrollFoot ui-state-default",
		"sScrollFootInner": "dataTables_scrollFootInner",

		/* Misc */
		"sFooterTH": "ui-state-default"
	};

	/*
	 * Variable: oPagination
	 * Purpose:  Container for the various type of pagination that dataTables supports
	 * Scope:    jQuery.fn.dataTableExt
	 */
	_oExt.oPagination = {
		/*
		 * Variable: two_button
		 * Purpose:  Standard two button (forward/back) pagination
	 	 * Scope:    jQuery.fn.dataTableExt.oPagination
		 */
		"two_button": {
			/*
			 * Function: oPagination.two_button.fnInit
			 * Purpose:  Initalise dom elements required for pagination with forward/back buttons only
			 * Returns:  -
	 		 * Inputs:   object:oSettings - dataTables settings object
	     *           node:nPaging - the DIV which contains this pagination control
			 *           function:fnCallbackDraw - draw function which must be called on update
			 */
			"fnInit": function (oSettings, nPaging, fnCallbackDraw) {
				var nPrevious, nNext, nPreviousInner, nNextInner;

				/* Store the next and previous elements in the oSettings object as they can be very
				 * usful for automation - particularly testing
				 */
				if (!oSettings.bJUI) {
					nPrevious = document.createElement('div');
					nNext = document.createElement('div');
				}
				else {
					nPrevious = document.createElement('a');
					nNext = document.createElement('a');

					nNextInner = document.createElement('span');
					nNextInner.className = oSettings.oClasses.sPageJUINext;
					nNext.appendChild(nNextInner);

					nPreviousInner = document.createElement('span');
					nPreviousInner.className = oSettings.oClasses.sPageJUIPrev;
					nPrevious.appendChild(nPreviousInner);
				}

				nPrevious.className = oSettings.oClasses.sPagePrevDisabled;
				nNext.className = oSettings.oClasses.sPageNextDisabled;

				nPrevious.title = oSettings.oLanguage.oPaginate.sPrevious;
				nNext.title = oSettings.oLanguage.oPaginate.sNext;

				nPaging.appendChild(nPrevious);
				nPaging.appendChild(nNext);

				$(nPrevious).click(function () {
					if (oSettings.oApi._fnPageChange(oSettings, "previous")) {
						/* Only draw when the page has actually changed */
						fnCallbackDraw(oSettings);
					}
				});

				$(nNext).click(function () {
					if (oSettings.oApi._fnPageChange(oSettings, "next")) {
						fnCallbackDraw(oSettings);
					}
				});

				/* Take the brutal approach to cancelling text selection 采用残忍的方法取消文本选择 */
				$(nPrevious).bind('selectstart', function () { return false;  });
				$(nNext).bind('selectstart', function () { return false; });

				/* ID the first elements only */
				if (oSettings.sTableId !== '' && typeof oSettings.aanFeatures.p == "undefined") {
					nPaging.setAttribute('id', oSettings.sTableId + '_paginate');
					nPrevious.setAttribute('id', oSettings.sTableId + '_previous');
					nNext.setAttribute('id', oSettings.sTableId + '_next');
				}
			},

			/*
			 * Function: oPagination.two_button.fnUpdate
			 * Purpose:  Update the two button pagination at the end of the draw
			 * Returns:  -
	 		 * Inputs:   object:oSettings - dataTables settings object
			 *           function:fnCallbackDraw - draw function to call on page change
			 */
			"fnUpdate": function (oSettings, fnCallbackDraw) {
				if (!oSettings.aanFeatures.p) { // 翻页节点不存在
					return;
				}

				/* Loop over each instance of the pager  遍历每个实例*/
				var an = oSettings.aanFeatures.p;
				for (var i = 0, iLen = an.length; i < iLen; i++) {
					if (an[i].childNodes.length !== 0) {
						an[i].childNodes[0].className =
							(oSettings._iDisplayStart === 0) ?
								oSettings.oClasses.sPagePrevDisabled : oSettings.oClasses.sPagePrevEnabled;

						an[i].childNodes[1].className =
							(oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay()) ?
								oSettings.oClasses.sPageNextDisabled : oSettings.oClasses.sPageNextEnabled;
					}
				}
			}
		},


		/*
		 * Variable: iFullNumbersShowPages
		 * Purpose:  Change the number of pages which can be seen
	 	 * Scope:    jQuery.fn.dataTableExt.oPagination
		 */
		"iFullNumbersShowPages": 5,

		/*
		 * Variable: full_numbers
		 * Purpose:  Full numbers pagination
	 	 * Scope:    jQuery.fn.dataTableExt.oPagination
		 */
		"full_numbers": {
			/*
			 * Function: oPagination.full_numbers.fnInit
			 * Purpose:  Initalise dom elements required for pagination with a list of the pages
			 * Returns:  -
	 		 * Inputs:   object:oSettings - dataTables settings object
	     *           node:nPaging - the DIV which contains this pagination control
			 *           function:fnCallbackDraw - draw function which must be called on update
			 */
			"fnInit": function (oSettings, nPaging, fnCallbackDraw) {
				var nFirst = document.createElement('span');
				var nPrevious = document.createElement('span');
				var nList = document.createElement('span');
				var nNext = document.createElement('span');
				var nLast = document.createElement('span');

				nFirst.innerHTML = oSettings.oLanguage.oPaginate.sFirst;
				nPrevious.innerHTML = oSettings.oLanguage.oPaginate.sPrevious;
				nNext.innerHTML = oSettings.oLanguage.oPaginate.sNext;
				nLast.innerHTML = oSettings.oLanguage.oPaginate.sLast;

				var oClasses = oSettings.oClasses;
				nFirst.className = oClasses.sPageButton + " " + oClasses.sPageFirst;
				nPrevious.className = oClasses.sPageButton + " " + oClasses.sPagePrevious;
				nNext.className = oClasses.sPageButton + " " + oClasses.sPageNext;
				nLast.className = oClasses.sPageButton + " " + oClasses.sPageLast;

				nPaging.appendChild(nFirst);
				nPaging.appendChild(nPrevious);
				nPaging.appendChild(nList);
				nPaging.appendChild(nNext);
				nPaging.appendChild(nLast);

				$(nFirst).click(function () {
					if (oSettings.oApi._fnPageChange(oSettings, "first")) {
						fnCallbackDraw(oSettings);
					}
				});

				$(nPrevious).click(function () {
					if (oSettings.oApi._fnPageChange(oSettings, "previous")) {
						fnCallbackDraw(oSettings);
					}
				});

				$(nNext).click(function () {
					if (oSettings.oApi._fnPageChange(oSettings, "next")) {
						fnCallbackDraw(oSettings);
					}
				});

				$(nLast).click(function () {
					if (oSettings.oApi._fnPageChange(oSettings, "last")) {
						fnCallbackDraw(oSettings);
					}
				});

				/* Take the brutal approach to cancelling text selection */
				$('span', nPaging)
					.bind('mousedown', function () { return false; })
					.bind('selectstart', function () { return false; });

				/* ID the first elements only */
				if (oSettings.sTableId !== '' && typeof oSettings.aanFeatures.p == "undefined") {
					nPaging.setAttribute('id', oSettings.sTableId + '_paginate');
					nFirst.setAttribute('id', oSettings.sTableId + '_first');
					nPrevious.setAttribute('id', oSettings.sTableId + '_previous');
					nNext.setAttribute('id', oSettings.sTableId + '_next');
					nLast.setAttribute('id', oSettings.sTableId + '_last');
				}
			},

			/*
			 * Function: oPagination.full_numbers.fnUpdate
			 * Purpose:  Update the list of page buttons shows
			 * Returns:  -
	 		 * Inputs:   object:oSettings - dataTables settings object
			 *           function:fnCallbackDraw - draw function to call on page change
			 */
			"fnUpdate": function (oSettings, fnCallbackDraw) {
				if (!oSettings.aanFeatures.p) {
					return;
				}

				var iPageCount = _oExt.oPagination.iFullNumbersShowPages;
				var iPageCountHalf = Math.floor(iPageCount / 2);
				var iPages = Math.ceil((oSettings.fnRecordsDisplay()) / oSettings._iDisplayLength);
				var iCurrentPage = Math.ceil(oSettings._iDisplayStart / oSettings._iDisplayLength) + 1;
				var sList = "";
				var iStartButton, iEndButton, i, iLen;
				var oClasses = oSettings.oClasses;

				/* Pages calculation */
				if (iPages < iPageCount) {
					iStartButton = 1;
					iEndButton = iPages;
				}
				else {
					if (iCurrentPage <= iPageCountHalf) {
						iStartButton = 1;
						iEndButton = iPageCount;
					}
					else {
						if (iCurrentPage >= (iPages - iPageCountHalf)) {
							iStartButton = iPages - iPageCount + 1;
							iEndButton = iPages;
						}
						else {
							iStartButton = iCurrentPage - Math.ceil(iPageCount / 2) + 1;
							iEndButton = iStartButton + iPageCount - 1;
						}
					}
				}

				/* Build the dynamic list */
				for (i = iStartButton; i <= iEndButton; i++) {
					if (iCurrentPage != i) {
						sList += '<span class="' + oClasses.sPageButton + '">' + i + '</span>';
					}
					else {
						sList += '<span class="' + oClasses.sPageButtonActive + '">' + i + '</span>';
					}
				}

				/* Loop over each instance of the pager */
				var an = oSettings.aanFeatures.p;
				var anButtons, anStatic, nPaginateList;
				var fnClick = function () {
					/* Use the information in the element to jump to the required page */
					var iTarget = (this.innerHTML * 1) - 1;
					oSettings._iDisplayStart = iTarget * oSettings._iDisplayLength;
					fnCallbackDraw(oSettings);
					return false;
				};
				var fnFalse = function () { return false; };

				for (i = 0, iLen = an.length; i < iLen; i++) {
					if (an[i].childNodes.length === 0) {
						continue;
					}

					/* Build up the dynamic list forst - html and listeners */
					nPaginateList = an[i].childNodes[2];
					nPaginateList.innerHTML = sList;

					$('span', nPaginateList).click(fnClick).bind('mousedown', fnFalse)
						.bind('selectstart', fnFalse);

					/* Update the 'premanent botton's classes */
					anButtons = an[i].getElementsByTagName('span');
					anStatic = [
						anButtons[0], anButtons[1],
						anButtons[anButtons.length - 2], anButtons[anButtons.length - 1]
					];
					$(anStatic).removeClass(oClasses.sPageButton + " " + oClasses.sPageButtonActive + " " + oClasses.sPageButtonStaticDisabled);
					if (iCurrentPage == 1) {
						anStatic[0].className += " " + oClasses.sPageButtonStaticDisabled;
						anStatic[1].className += " " + oClasses.sPageButtonStaticDisabled;
					}
					else {
						anStatic[0].className += " " + oClasses.sPageButton;
						anStatic[1].className += " " + oClasses.sPageButton;
					}

					if (iPages === 0 || iCurrentPage == iPages || oSettings._iDisplayLength == -1) {
						anStatic[2].className += " " + oClasses.sPageButtonStaticDisabled;
						anStatic[3].className += " " + oClasses.sPageButtonStaticDisabled;
					}
					else {
						anStatic[2].className += " " + oClasses.sPageButton;
						anStatic[3].className += " " + oClasses.sPageButton;
					}
				}
			}
		}
	};

	/*
	 * Variable: oSort
	 * Purpose:  Wrapper for the sorting functions that can be used in DataTables
	 * Scope:    jQuery.fn.dataTableExt
	 * Notes:    The functions provided in this object are basically standard javascript sort
	 *   functions - they expect two inputs which they then compare and then return a priority
	 *   result. For each sort method added, two functions need to be defined, an ascending sort and
	 *   a descending sort.
	 */
	_oExt.oSort = {
		/*
		 * text sorting
		 */
		"string-asc": function (a, b) {
			var x = a.toLowerCase();
			var y = b.toLowerCase();
			return ((x < y) ? -1 : ((x > y) ? 1 : 0));
		},

		"string-desc": function (a, b) {
			var x = a.toLowerCase();
			var y = b.toLowerCase();
			return ((x < y) ? 1 : ((x > y) ? -1 : 0));
		},


		/*
		 * html sorting (ignore html tags)
		 */
		"html-asc": function (a, b) {
			var x = a.replace(/<.*?>/g, "").toLowerCase();
			var y = b.replace(/<.*?>/g, "").toLowerCase();
			return ((x < y) ? -1 : ((x > y) ? 1 : 0));
		},

		"html-desc": function (a, b) {
			var x = a.replace(/<.*?>/g, "").toLowerCase();
			var y = b.replace(/<.*?>/g, "").toLowerCase();
			return ((x < y) ? 1 : ((x > y) ? -1 : 0));
		},


		/*
		 * date sorting
		 */
		"date-asc": function (a, b) {
			var x = Date.parse(a);
			var y = Date.parse(b);

			if (isNaN(x) || x === "") {
				x = Date.parse("01/01/1970 00:00:00");
			}
			if (isNaN(y) || y === "") {
				y = Date.parse("01/01/1970 00:00:00");
			}

			return x - y;
		},

		"date-desc": function (a, b) {
			var x = Date.parse(a);
			var y = Date.parse(b);

			if (isNaN(x) || x === "") {
				x = Date.parse("01/01/1970 00:00:00");
			}
			if (isNaN(y) || y === "") {
				y = Date.parse("01/01/1970 00:00:00");
			}

			return y - x;
		},


		/*
		 * numerical sorting
		 */
		"numeric-asc": function (a, b) {
			var x = (a == "-" || a === "") ? 0 : a * 1;
			var y = (b == "-" || b === "") ? 0 : b * 1;
			return x - y;
		},

		"numeric-desc": function (a, b) {
			var x = (a == "-" || a === "") ? 0 : a * 1;
			var y = (b == "-" || b === "") ? 0 : b * 1;
			return y - x;
		}
	};


	/*
	 * Variable: aTypes
	 * Purpose:  Container for the various type of type detection that dataTables supports
	 * Scope:    jQuery.fn.dataTableExt
	 * Notes:    The functions in this array are expected to parse a string to see if it is a data
	 *   type that it recognises. If so then the function should return the name of the type (a
	 *   corresponding sort function should be defined!), if the type is not recognised then the
	 *   function should return null such that the parser and move on to check the next type.
	 *   Note that ordering is important in this array - the functions are processed linearly,
	 *   starting at index 0.
	 *   Note that the input for these functions is always a string! It cannot be any other data
	 *   type
	 */
	_oExt.aTypes = [
		/*
		 * Function: -
		 * Purpose:  Check to see if a string is numeric
		 * Returns:  string:'numeric' or null
		 * Inputs:   string:sText - string to check
		 */
		function (sData) {
			/* Allow zero length strings as a number */
			if (sData.length === 0) {
				return 'numeric';
			}

			var sValidFirstChars = "0123456789-";
			var sValidChars = "0123456789.";
			var Char;
			var bDecimal = false;

			/* Check for a valid first char (no period and allow negatives) */
			Char = sData.charAt(0);
			if (sValidFirstChars.indexOf(Char) == -1) {
				return null;
			}

			/* Check all the other characters are valid */
			for (var i = 1; i < sData.length; i++) {
				Char = sData.charAt(i);
				if (sValidChars.indexOf(Char) == -1) {
					return null;
				}

				/* Only allowed one decimal place... */
				if (Char == ".") {
					if (bDecimal) {
						return null;
					}
					bDecimal = true;
				}
			}

			return 'numeric';
		},

		/*
		 * Function: -
		 * Purpose:  Check to see if a string is actually a formatted date
		 * Returns:  string:'date' or null
		 * Inputs:   string:sText - string to check
		 */
		function (sData) {
			var iParse = Date.parse(sData);
			if ((iParse !== null && !isNaN(iParse)) || sData.length === 0) {
				return 'date';
			}
			return null;
		},

		/*
		 * Function: -
		 * Purpose:  Check to see if a string should be treated as an HTML string
		 * Returns:  string:'html' or null
		 * Inputs:   string:sText - string to check
		 */
		function (sData) {
			if (sData.indexOf('<') != -1 && sData.indexOf('>') != -1) {
				return 'html';
			}
			return null;
		}
	];

	/*
	 * Function: fnVersionCheck
	 * Purpose:  Check a version string against this version of DataTables. Useful for plug-ins
	 * Returns:  bool:true -this version of DataTables is greater or equal to the required version
	 *                false -this version of DataTales is not suitable
	 * Inputs:   string:sVersion - the version to check against. May be in the following formats:
	 *             "a", "a.b" or "a.b.c"
	 * Notes:    This function will only check the first three parts of a version string. It is
	 *   assumed that beta and dev versions will meet the requirements. This might change in future
	 */
	_oExt.fnVersionCheck = function (sVersion) {
		/* This is cheap, but very effective */
		var fnZPad = function (Zpad, count) {
			while (Zpad.length < count) {
				Zpad += '0';
			}
			return Zpad;
		};
		var aThis = _oExt.sVersion.split('.');
		var aThat = sVersion.split('.');
		var sThis = '', sThat = '';

		for (var i = 0, iLen = aThat.length; i < iLen; i++) {
			sThis += fnZPad(aThis[i], 3);
			sThat += fnZPad(aThat[i], 3);
		}

		return parseInt(sThis, 10) >= parseInt(sThat, 10);
	};

	/*
	 * Variable: _oExternConfig
	 * Purpose:  Store information for DataTables to access globally about other instances
	 * Scope:    jQuery.fn.dataTableExt
	 */
	_oExt._oExternConfig = {
		/* int:iNextUnique - next unique number for an instance */
		"iNextUnique": 0
	};


	/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
	 * Section - DataTables prototype
	 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

	/*
	 * Function: dataTable
	 * Purpose:  DataTables information
	 * Returns:  -
	 * Inputs:   object:oInit - initalisation options for the table
	 */
	$.fn.dataTable = function (oInit) {
		/*
		 * Function: classSettings
		 * Purpose:  Settings container function for all 'class' properties which are required
		 *   by dataTables
		 * Returns:  -
		 * Inputs:   -
		 */
		function classSettings() {
			this.fnRecordsTotal = function () {
				if (this.oFeatures.bServerSide) {
					return this._iRecordsTotal;
				} else {
					return this.aiDisplayMaster.length;
				}
			};

			this.fnRecordsDisplay = function () {
				if (this.oFeatures.bServerSide) {
					return this._iRecordsDisplay;
				} else {
					return this.aiDisplay.length;
				}
			};

			this.fnDisplayEnd = function () {
				if (this.oFeatures.bServerSide) {
					if (this.oFeatures.bPaginate === false) {
						return this._iDisplayStart + this.aiDisplay.length;
					} else {
						return Math.min(this._iDisplayStart + this._iDisplayLength,
							this._iDisplayStart + this.aiDisplay.length);
					}
				} else {
					return this._iDisplayEnd;
				}
			};

			/*
			 * Variable: oInstance
			 * Purpose:  The DataTables object for this table   DataTables对象
			 * Scope:    jQuery.dataTable.classSettings 
			 */
			this.oInstance = null;

			/*
			 * Variable: sInstance
			 * Purpose:  Unique idendifier for each instance of the DataTables object  
			 * Scope:    jQuery.dataTable.classSettings 
			 */
			this.sInstance = null;

			/*
			 * Variable: oFeatures
			 * Purpose:  Indicate the enablement of key dataTable features
			 * Scope:    jQuery.dataTable.classSettings 
			 */
			this.oFeatures = {
				"bPaginate": true,
				"bLengthChange": true,
				"bFilter": true,
				"bSort": true,
				"bInfo": true,
				"bAutoWidth": true,
				"bProcessing": false,
				"bSortClasses": true,
				"bStateSave": false,
				"bServerSide": false
			};

			/*
			 * Variable: oScroll
			 * Purpose:  Container for scrolling options
			 * Scope:    jQuery.dataTable.classSettings 
			 */
			this.oScroll = {
				"sX": "",
				"sXInner": "",
				"sY": "",
				"bCollapse": false,
				"iBarWidth": 0
			};

			/*
			 * Variable: aanFeatures
			 * Purpose:  Array referencing the nodes which are used for the features   引用用于功能的节点的数组
			 * Scope:    jQuery.dataTable.classSettings 
			 * Notes:    The parameters of this object match what is allowed by sDom - i.e.
			 *   'l' - Length changing
			 *   'f' - Filtering input
			 *   't' - The table!
			 *   'i' - Information
			 *   'p' - Pagination
			 *   'r' - pRocessing
			 */
			this.aanFeatures = [];

			/*
			 * Variable: oLanguage
			 * Purpose:  Store the language strings used by dataTables
			 * Scope:    jQuery.dataTable.classSettings
			 * Notes:    The words in the format _VAR_ are variables which are dynamically replaced
			 *   by javascript
			 */
			this.oLanguage = {
				"sProcessing": "Processing...",
				"sLengthMenu": "Show _MENU_ entries",
				"sZeroRecords": "No matching records found",
				"sEmptyTable": "No data available in table",
				"sInfo": "Showing _START_ to _END_ of _TOTAL_ entries",
				"sInfoEmpty": "Showing 0 to 0 of 0 entries",
				"sInfoFiltered": "(filtered from _MAX_ total entries)",
				"sInfoPostFix": "",
				"sSearch": "Search:",
				"sUrl": "",
				"oPaginate": {
					"sFirst": "First",
					"sPrevious": "Previous",
					"sNext": "Next",
					"sLast": "Last"
				}
			};

			/*
			 * Variable: aoData
			 * Purpose:  Store data information
			 * Scope:    jQuery.dataTable.classSettings 
			 * Notes:    This is an array of objects with the following parameters:
			 *   int: _iId - internal id for tracking
			 *   array: _aData - internal data - used for sorting / filtering etc
			 *   node: nTr - display node
			 *   array node: _anHidden - hidden TD nodes
			 *   string: _sRowStripe
			 */
			this.aoData = [];

			/*
			 * Variable: aiDisplay   当前显示中的索引数组（过滤后等）
			 * Purpose:  Array of indexes which are in the current display (after filtering etc)
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.aiDisplay = [];

			/*
			 * Variable: aiDisplayMaster
			 * Purpose:  Array of indexes for display - no filtering
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.aiDisplayMaster = [];

			/*
			 * Variable: aoColumns
			 * Purpose:  Store information about each column that is in use
			 * Scope:    jQuery.dataTable.classSettings 
			 */
			this.aoColumns = [];

			/*
			 * Variable: iNextId
			 * Purpose:  Store the next unique id to be used for a new row
			 * Scope:    jQuery.dataTable.classSettings 
			 */
			this.iNextId = 0;

			/*
			 * Variable: asDataSearch
			 * Purpose:  Search data array for regular expression searching
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.asDataSearch = [];

			/*
			 * Variable: oPreviousSearch
			 * Purpose:  Store the previous search incase we want to force a re-search
			 *   or compare the old search to a new one
			 * 存储以前的搜索，以防我们要强制重新搜索或将旧搜索与新搜索进行比较。
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.oPreviousSearch = {
				"sSearch": "",
				"bRegex": false,
				"bSmart": true
			};

			/*
			 * Variable: aoPreSearchCols
			 * Purpose:  Store the previous search for each column
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.aoPreSearchCols = [];

			/*
			 * Variable: aaSorting
			 * Purpose:  Sorting information
			 * Scope:    jQuery.dataTable.classSettings
			 * Notes:    Index 0 - column number
			 *           Index 1 - current sorting direction
			 *           Index 2 - index of asSorting for this column
			 */
			this.aaSorting = [[0, 'asc', 0]];

			/*
			 * Variable: aaSortingFixed
			 * Purpose:  Sorting information that is always applied
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.aaSortingFixed = null;

			/*
			 * Variable: asStripClasses
			 * Purpose:  Classes to use for the striping of a table
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.asStripClasses = [];

			/*
			 * Variable: asDestoryStrips
			 * Purpose:  If restoring a table - we should restore it's striping classes as well
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.asDestoryStrips = [];

			/*
			 * Variable: fnRowCallback
			 * Purpose:  Call this function every time a row is inserted (draw)
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.fnRowCallback = null;

			/*
			 * Variable: fnHeaderCallback
			 * Purpose:  Callback function for the header on each draw
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.fnHeaderCallback = null;

			/*
			 * Variable: fnFooterCallback
			 * Purpose:  Callback function for the footer on each draw
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.fnFooterCallback = null;

			/*
			 * Variable: aoDrawCallback
			 * Purpose:  Array of callback functions for draw callback functions
			 * 绘制回调函数的回调函数数组
			 * Scope:    jQuery.dataTable.classSettings
			 * Notes:    Each array element is an object with the following parameters:
			 *   function:fn - function to call
			 *   string:sName - name callback (feature). useful for arranging array
			 */
			this.aoDrawCallback = [];

			/*
			 * Variable: fnInitComplete
			 * Purpose:  Callback function for when the table has been initalised
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.fnInitComplete = null;

			/*
			 * Variable: sTableId
			 * Purpose:  Cache the table ID for quick access
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.sTableId = "";

			/*
			 * Variable: nTable
			 * Purpose:  Cache the table node for quick access
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.nTable = null;

			/*
			 * Variable: nTHead
			 * Purpose:  Permanent ref to the thead element
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.nTHead = null;

			/*
			 * Variable: nTFoot
			 * Purpose:  Permanent ref to the tfoot element - if it exists
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.nTFoot = null;

			/*
			 * Variable: nTBody
			 * Purpose:  Permanent ref to the tbody element
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.nTBody = null;

			/*
			 * Variable: nTableWrapper
			 * Purpose:  Cache the wrapper node (contains all DataTables controlled elements)
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.nTableWrapper = null;

			/*
			 * Variable: iDefaultSortIndex
			 * Purpose:  Sorting index which will be used by default
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.iDefaultSortIndex = 0;

			/*
			 * Variable: bInitialised
			 * Purpose:  Indicate if all required information has been read in
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.bInitialised = false;

			/*
			 * Variable: aoOpenRows
			 * Purpose:  Information about open rows
			 * Scope:    jQuery.dataTable.classSettings
			 * Notes:    Has the parameters 'nTr' and 'nParent'
			 */
			this.aoOpenRows = [];

			/*
			 * Variable: sDom
			 * Purpose:  Dictate the positioning that the created elements will take
			 * Scope:    jQuery.dataTable.classSettings
			 * Notes:    
			 *   The following options are allowed:
			 *     'l' - Length changing
			 *     'f' - Filtering input
			 *     't' - The table!
			 *     'i' - Information
			 *     'p' - Pagination
			 *     'r' - pRocessing
			 *   The following constants are allowed:
			 *     'H' - jQueryUI theme "header" classes
			 *     'F' - jQueryUI theme "footer" classes
			 *   The following syntax is expected:
			 *     '<' and '>' - div elements
			 *     '<"class" and '>' - div with a class
			 *   Examples:
			 *     '<"wrapper"flipt>', '<lf<t>ip>'
			 */
			this.sDom = 'lfrtip';

			/*
			 * Variable: sPaginationType
			 * Purpose:  Note which type of sorting should be used
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.sPaginationType = "two_button";

			/*
			 * Variable: iCookieDuration
			 * Purpose:  The cookie duration (for bStateSave) in seconds - default 2 hours
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.iCookieDuration = 60 * 60 * 2;

			/*
			 * Variable: sCookiePrefix
			 * Purpose:  The cookie name prefix
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.sCookiePrefix = "SpryMedia_DataTables_";

			/*
			 * Variable: sAjaxSource
			 * Purpose:  Source url for AJAX data for the table
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.sAjaxSource = null;

			/*
			 * Variable: bAjaxDataGet
			 * Purpose:  Note if draw should be blocked while getting data
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.bAjaxDataGet = true;

			/*
			 * Variable: fnServerData
			 * Purpose:  Function to get the server-side data - can be overruled by the developer
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.fnServerData = function (url, data, callback) {
				$.ajax({
					"url": url,
					"data": data,
					"success": callback,
					"dataType": "json",
					"cache": false,
					"error": function () {
						alert("DataTables warning: JSON data from server failed to load or be parsed. " +
							"This is most likely to be caused by a JSON formatting error.");
					}
				});
			};

			/*
			 * Variable: fnFormatNumber
			 * Purpose:  Format numbers for display， 转换成千分位显示
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.fnFormatNumber = function (iIn) {
				if (iIn < 1000) {
					/* A small optimisation for what is likely to be the vast majority of use cases */
					return iIn;
				}
				else {
					var s = (iIn + ""), a = s.split(""), out = "", iLen = s.length;

					for (var i = 0; i < iLen; i++) {
						if (i % 3 === 0 && i !== 0) {
							out = ',' + out;
						}
						out = a[iLen - i - 1] + out;
					}
				}
				return out;
			};

			/*
			 * Variable: aLengthMenu
			 * Purpose:  List of options that can be used for the user selectable length menu
			 * Scope:    jQuery.dataTable.classSettings
			 * Note:     This varaible can take for form of a 1D array, in which case the value and the 
			 *   displayed value in the menu are the same, or a 2D array in which case the value comes
			 *   from the first array, and the displayed value to the end user comes from the second
			 *   array. 2D example: [ [ 10, 25, 50, 100, -1 ], [ 10, 25, 50, 100, 'All' ] ];
			 */
			this.aLengthMenu = [10, 25, 50, 100];

			/*
			 * Variable: iDraw
			 * Purpose:  Counter for the draws that the table does. Also used as a tracker for
			 *   server-side processing
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.iDraw = 0;

			/*
			 * Variable: iDrawError
			 * Purpose:  Last draw error
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.iDrawError = -1;

			/*
			 * Variable: _iDisplayLength, _iDisplayStart, _iDisplayEnd
			 * Purpose:  Display length variables
			 * Scope:    jQuery.dataTable.classSettings
			 * Notes:    These variable must NOT be used externally to get the data length. Rather, use
			 *   the fnRecordsTotal() (etc) functions.
			 * 不能在外部使用这些变量来获取数据长度。相反，使用 fnrecordstotal（）（etc）函数
			 */
			this._iDisplayLength = 10;
			this._iDisplayStart = 0;
			this._iDisplayEnd = 10;

			/*
			 * Variable: _iRecordsTotal, _iRecordsDisplay
			 * Purpose:  Display length variables used for server side processing
			 * Scope:    jQuery.dataTable.classSettings
			 * Notes:    These variable must NOT be used externally to get the data length. Rather, use
			 *   the fnRecordsTotal() (etc) functions.
			 */
			this._iRecordsTotal = 0;
			this._iRecordsDisplay = 0;

			/*
			 * Variable: bJUI
			 * Purpose:  Should we add the markup needed for jQuery UI theming?
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.bJUI = false;

			/*
			 * Variable: bJUI
			 * Purpose:  Should we add the markup needed for jQuery UI theming?
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.oClasses = _oExt.oStdClasses;

			/*
			 * Variable: bFiltered and bSorted
			 * Purpose:  Flags to allow callback functions to see what actions have been performed
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.bFiltered = false;
			this.bSorted = false;

			/*
			 * Variable: oInit
			 * Purpose:  Initialisation object that is used for the table
			 * Scope:    jQuery.dataTable.classSettings
			 */
			this.oInit = null;
		}

		/*
		 * Variable: oApi
		 * Purpose:  Container for publicly exposed 'private' functions
		 * Scope:    jQuery.dataTable
		 */
		this.oApi = {};


		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - API functions
		 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

		/*
		 * Function: fnDraw
		 * Purpose:  Redraw the table
		 * Returns:  -
		 * Inputs:   bool:bComplete - Refilter and resort (if enabled) the table before the draw.
		 *             Optional: default - true
		 */
		this.fnDraw = function (bComplete) {
			var oSettings = _fnSettingsFromNode(this[_oExt.iApiIndex]);
			if (typeof bComplete != 'undefined' && bComplete === false) {
				_fnCalculateEnd(oSettings);
				_fnDraw(oSettings);
			}
			else {
				_fnReDraw(oSettings);
			}
		};

		/*
		 * Function: fnFilter
		 * Purpose:  Filter the input based on data
		 * Returns:  -
		 * Inputs:   string:sInput - string to filter the table on
		 *           int:iColumn - optional - column to limit filtering to
		 *           bool:bRegex - optional - treat as regular expression or not - default false
		 *           bool:bSmart - optional - perform smart filtering or not - default true
		 *           bool:bShowGlobal - optional - show the input global filter in it's input box(es)
		 *              - default true
		 */
		this.fnFilter = function (sInput, iColumn, bRegex, bSmart, bShowGlobal) {
			var oSettings = _fnSettingsFromNode(this[_oExt.iApiIndex]);

			if (!oSettings.oFeatures.bFilter) {
				return;
			}

			if (typeof bRegex == 'undefined') {
				bRegex = false;
			}

			if (typeof bSmart == 'undefined') {
				bSmart = true;
			}

			if (typeof bShowGlobal == 'undefined') {
				bShowGlobal = true;
			}

			if (typeof iColumn == "undefined" || iColumn === null) {
				/* Global filter */
				_fnFilterComplete(oSettings, {
					"sSearch": sInput,
					"bRegex": bRegex,
					"bSmart": bSmart
				}, 1);

				if (bShowGlobal && typeof oSettings.aanFeatures.f != 'undefined') {
					var n = oSettings.aanFeatures.f;
					for (var i = 0, iLen = n.length; i < iLen; i++) {
						$('input', n[i]).val(sInput);
					}
				}
			}
			else {
				/* Single column filter */
				oSettings.aoPreSearchCols[iColumn].sSearch = sInput;
				oSettings.aoPreSearchCols[iColumn].bRegex = bRegex;
				oSettings.aoPreSearchCols[iColumn].bSmart = bSmart;
				_fnFilterComplete(oSettings, oSettings.oPreviousSearch, 1);
			}
		};

		/*
		 * Function: fnSettings
		 * Purpose:  Get the settings for a particular table for extern. manipulation
		 * Returns:  -
		 * Inputs:   -
		 */
		this.fnSettings = function (nNode) {
			return _fnSettingsFromNode(this[_oExt.iApiIndex]);
		};

		/*
		 * Function: fnVersionCheck
		 * Notes:    The function is the same as the 'static' function provided in the ext variable
		 */
		this.fnVersionCheck = _oExt.fnVersionCheck;

		/*
		 * Function: fnSort
		 * Purpose:  Sort the table by a particular row
		 * Returns:  -
		 * Inputs:   int:iCol - the data index to sort on. Note that this will
		 *   not match the 'display index' if you have hidden data entries
		 */
		this.fnSort = function (aaSort) {
			var oSettings = _fnSettingsFromNode(this[_oExt.iApiIndex]);
			oSettings.aaSorting = aaSort;
			_fnSort(oSettings);
		};

		/*
		 * Function: fnSortListener
		 * Purpose:  Attach a sort listener to an element for a given column
		 * Returns:  -
		 * Inputs:   node:nNode - the element to attach the sort listener to
		 *           int:iColumn - the column that a click on this node will sort on
		 *           function:fnCallback - callback function when sort is run - optional
		 */
		this.fnSortListener = function (nNode, iColumn, fnCallback) {
			_fnSortAttachListener(_fnSettingsFromNode(this[_oExt.iApiIndex]), nNode, iColumn,
				fnCallback);
		};

		/*
		 * Function: fnAddData
		 * Purpose:  Add new row(s) into the table
		 * Returns:  array int: array of indexes (aoData) which have been added (zero length on error)
		 * Inputs:   array:mData - the data to be added. The length must match
		 *               the original data from the DOM
		 *             or
		 *             array array:mData - 2D array of data to be added
		 *           bool:bRedraw - redraw the table or not - default true
		 * Notes:    Warning - the refilter here will cause the table to redraw
		 *             starting at zero
		 * Notes:    Thanks to Yekimov Denis for contributing the basis for this function!
		 */
		this.fnAddData = function (mData, bRedraw) {
			if (mData.length === 0) {
				return [];
			}

			var aiReturn = [];
			var iTest;

			/* Find settings from table node */
			var oSettings = _fnSettingsFromNode(this[_oExt.iApiIndex]);

			/* Check if we want to add multiple rows or not */
			if (typeof mData[0] == "object") {
				for (var i = 0; i < mData.length; i++) {
					iTest = _fnAddData(oSettings, mData[i]);
					if (iTest == -1) {
						return aiReturn;
					}
					aiReturn.push(iTest);
				}
			}
			else {
				iTest = _fnAddData(oSettings, mData);
				if (iTest == -1) {
					return aiReturn;
				}
				aiReturn.push(iTest);
			}

			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();

			/* Rebuild the search */
			_fnBuildSearchArray(oSettings, 1);

			if (typeof bRedraw == 'undefined' || bRedraw) {
				_fnReDraw(oSettings);
			}
			return aiReturn;
		};

		/*
		 * Function: fnDeleteRow
		 * Purpose:  Remove a row for the table
		 * Returns:  array:aReturn - the row that was deleted
		 * Inputs:   mixed:mTarget - 
		 *             int: - index of aoData to be deleted, or
		 *             node(TR): - TR element you want to delete
		 *           function:fnCallBack - callback function - default null
		 *           bool:bRedraw - redraw the table or not - default true
		 */
		this.fnDeleteRow = function (mTarget, fnCallBack, bRedraw) {
			/* Find settings from table node */
			var oSettings = _fnSettingsFromNode(this[_oExt.iApiIndex]);
			var i, iAODataIndex;

			iAODataIndex = (typeof mTarget == 'object') ?
				_fnNodeToDataIndex(oSettings, mTarget) : mTarget;

			/* Return the data array from this row */
			var oData = oSettings.aoData.splice(iAODataIndex, 1);

			/* Delete from the display arrays */
			_fnDeleteIndex(oSettings.aiDisplayMaster, iAODataIndex);
			_fnDeleteIndex(oSettings.aiDisplay, iAODataIndex);

			/* Rebuild the search */
			_fnBuildSearchArray(oSettings, 1);

			/* If there is a user callback function - call it */
			if (typeof fnCallBack == "function") {
				fnCallBack.call(this, oSettings, oData);
			}

			/* Check for an 'overflow' they case for dislaying the table */
			if (oSettings._iDisplayStart >= oSettings.aiDisplay.length) {
				oSettings._iDisplayStart -= oSettings._iDisplayLength;
				if (oSettings._iDisplayStart < 0) {
					oSettings._iDisplayStart = 0;
				}
			}

			if (typeof bRedraw == 'undefined' || bRedraw) {
				_fnCalculateEnd(oSettings);
				_fnDraw(oSettings);
			}

			return oData;
		};

		/*
		 * Function: fnClearTable
		 * Purpose:  Quickly and simply clear a table
		 * Returns:  -
		 * Inputs:   bool:bRedraw - redraw the table or not - default true
		 * Notes:    Thanks to Yekimov Denis for contributing the basis for this function!
		 */
		this.fnClearTable = function (bRedraw) {
			/* Find settings from table node */
			var oSettings = _fnSettingsFromNode(this[_oExt.iApiIndex]);
			_fnClearTable(oSettings);

			if (typeof bRedraw == 'undefined' || bRedraw) {
				_fnDraw(oSettings);
			}
		};

		/*
		 * Function: fnOpen
		 * Purpose:  Open a display row (append a row after the row in question)
		 * Returns:  node:nNewRow - the row opened
		 * Inputs:   node:nTr - the table row to 'open'
		 *           string:sHtml - the HTML to put into the row
		 *           string:sClass - class to give the new TD cell
		 */
		this.fnOpen = function (nTr, sHtml, sClass) {
			/* Find settings from table node */
			var oSettings = _fnSettingsFromNode(this[_oExt.iApiIndex]);

			/* the old open one if there is one */
			this.fnClose(nTr);

			var nNewRow = document.createElement("tr");
			var nNewCell = document.createElement("td");
			nNewRow.appendChild(nNewCell);
			nNewCell.className = sClass;
			nNewCell.colSpan = _fnVisbleColumns(oSettings);
			nNewCell.innerHTML = sHtml;

			/* If the nTr isn't on the page at the moment - then we don't insert at the moment */
			var nTrs = $('tr', oSettings.nTBody);
			if ($.inArray(nTr, nTrs) != -1) {
				$(nNewRow).insertAfter(nTr);
			}

			oSettings.aoOpenRows.push({
				"nTr": nNewRow,
				"nParent": nTr
			});

			return nNewRow;
		};

		/*
		 * Function: fnClose
		 * Purpose:  Close a display row
		 * Returns:  int: 0 (success) or 1 (failed)
		 * Inputs:   node:nTr - the table row to 'close'
		 */
		this.fnClose = function (nTr) {
			/* Find settings from table node */
			var oSettings = _fnSettingsFromNode(this[_oExt.iApiIndex]);

			for (var i = 0; i < oSettings.aoOpenRows.length; i++) {
				if (oSettings.aoOpenRows[i].nParent == nTr) {
					var nTrParent = oSettings.aoOpenRows[i].nTr.parentNode;
					if (nTrParent) {
						/* Remove it if it is currently on display */
						nTrParent.removeChild(oSettings.aoOpenRows[i].nTr);
					}
					oSettings.aoOpenRows.splice(i, 1);
					return 0;
				}
			}
			return 1;
		};

		/*
		 * Function: fnGetData
		 * Purpose:  Return an array with the data which is used to make up the table
		 * Returns:  array array string: 2d data array ([row][column]) or array string: 1d data array
		 *           or
		 *           array string (if iRow specified)
		 * Inputs:   mixed:mRow - optional - if not present, then the full 2D array for the table 
		 *             if given then:
		 *               int: - return 1D array for aoData entry of this index
		 *               node(TR): - return 1D array for this TR element
		 * Inputs:   int:iRow - optional - if present then the array returned will be the data for
		 *             the row with the index 'iRow'
		 */
		this.fnGetData = function (mRow) {
			var oSettings = _fnSettingsFromNode(this[_oExt.iApiIndex]);

			if (typeof mRow != 'undefined') {
				var iRow = (typeof mRow == 'object') ?
					_fnNodeToDataIndex(oSettings, mRow) : mRow;
				return oSettings.aoData[iRow]._aData;
			}
			return _fnGetDataMaster(oSettings);
		};

		/*
		 * Function: fnGetNodes
		 * Purpose:  Return an array with the TR nodes used for drawing the table
		 * Returns:  array node: TR elements
		 *           or
		 *           node (if iRow specified)
		 * Inputs:   int:iRow - optional - if present then the array returned will be the node for
		 *             the row with the index 'iRow'
		 */
		this.fnGetNodes = function (iRow) {
			var oSettings = _fnSettingsFromNode(this[_oExt.iApiIndex]);

			if (typeof iRow != 'undefined') {
				return oSettings.aoData[iRow].nTr;
			}
			return _fnGetTrNodes(oSettings);
		};

		/*
		 * Function: fnGetPosition
		 * Purpose:  Get the array indexes of a particular cell from it's DOM element
		 * Returns:  int: - row index, or array[ int, int, int ]: - row index, column index (visible)
		 *             and column index including hidden columns
		 * Inputs:   node:nNode - this can either be a TR or a TD in the table, the return is
		 *             dependent on this input
		 */
		this.fnGetPosition = function (nNode) {
			var oSettings = _fnSettingsFromNode(this[_oExt.iApiIndex]);
			var i;

			if (nNode.nodeName.toUpperCase() == "TR") {
				return _fnNodeToDataIndex(oSettings, nNode);
			}
			else if (nNode.nodeName.toUpperCase() == "TD") {
				var iDataIndex = _fnNodeToDataIndex(oSettings, nNode.parentNode);
				var iCorrector = 0;
				for (var j = 0; j < oSettings.aoColumns.length; j++) {
					if (oSettings.aoColumns[j].bVisible) {
						if (oSettings.aoData[iDataIndex].nTr.getElementsByTagName('td')[j - iCorrector] == nNode) {
							return [iDataIndex, j - iCorrector, j];
						}
					}
					else {
						iCorrector++;
					}
				}
			}
			return null;
		};

		/*
		 * Function: fnUpdate
		 * Purpose:  Update a table cell or row
		 * Returns:  int: 0 okay, 1 error
		 * Inputs:   array string 'or' string:mData - data to update the cell/row with
		 *           mixed:mRow - 
		 *             int: - index of aoData to be updated, or
		 *             node(TR): - TR element you want to update
		 *           int:iColumn - the column to update - optional (not used of mData is 2D)
		 *           bool:bRedraw - redraw the table or not - default true
		 *           bool:bAction - perform predraw actions or not (you will want this as 'true' if
		 *             you have bRedraw as true) - default true
		 */
		this.fnUpdate = function (mData, mRow, iColumn, bRedraw, bAction) {
			var oSettings = _fnSettingsFromNode(this[_oExt.iApiIndex]);
			var iVisibleColumn;
			var sDisplay;
			var iRow = (typeof mRow == 'object') ?
				_fnNodeToDataIndex(oSettings, mRow) : mRow;

			if (typeof mData != 'object') {
				sDisplay = mData;
				oSettings.aoData[iRow]._aData[iColumn] = sDisplay;

				if (oSettings.aoColumns[iColumn].fnRender !== null) {
					sDisplay = oSettings.aoColumns[iColumn].fnRender({
						"iDataRow": iRow,
						"iDataColumn": iColumn,
						"aData": oSettings.aoData[iRow]._aData,
						"oSettings": oSettings
					});

					if (oSettings.aoColumns[iColumn].bUseRendered) {
						oSettings.aoData[iRow]._aData[iColumn] = sDisplay;
					}
				}

				iVisibleColumn = _fnColumnIndexToVisible(oSettings, iColumn);
				if (iVisibleColumn !== null) {
					oSettings.aoData[iRow].nTr.getElementsByTagName('td')[iVisibleColumn].innerHTML =
						sDisplay;
				}
			}
			else {
				if (mData.length != oSettings.aoColumns.length) {
					_fnLog(oSettings, 0, 'An array passed to fnUpdate must have the same number of ' +
						'columns as the table in question - in this case ' + oSettings.aoColumns.length);
					return 1;
				}

				for (var i = 0; i < mData.length; i++) {
					sDisplay = mData[i];
					oSettings.aoData[iRow]._aData[i] = sDisplay;

					if (oSettings.aoColumns[i].fnRender !== null) {
						sDisplay = oSettings.aoColumns[i].fnRender({
							"iDataRow": iRow,
							"iDataColumn": i,
							"aData": oSettings.aoData[iRow]._aData,
							"oSettings": oSettings
						});

						if (oSettings.aoColumns[i].bUseRendered) {
							oSettings.aoData[iRow]._aData[i] = sDisplay;
						}
					}

					iVisibleColumn = _fnColumnIndexToVisible(oSettings, i);
					if (iVisibleColumn !== null) {
						oSettings.aoData[iRow].nTr.getElementsByTagName('td')[iVisibleColumn].innerHTML =
							sDisplay;
					}
				}
			}

			/* Perform pre-draw actions */
			if (typeof bAction == 'undefined' || bAction) {
				_fnBuildSearchArray(oSettings, 1);
				_fnAjustColumnSizing(oSettings);
			}

			/* Redraw the table */
			if (typeof bRedraw == 'undefined' || bRedraw) {
				_fnReDraw(oSettings);
			}
			return 0;
		};


		/*
		 * Function: fnShowColoumn
		 * Purpose:  Show a particular column
		 * Returns:  -
		 * Inputs:   int:iCol - the column whose display should be changed
		 *           bool:bShow - show (true) or hide (false) the column
		 */
		this.fnSetColumnVis = function (iCol, bShow) {
			var oSettings = _fnSettingsFromNode(this[_oExt.iApiIndex]);
			var i, iLen;
			var iColumns = oSettings.aoColumns.length;
			var nTd, anTds;

			/* No point in doing anything if we are requesting what is already true */
			if (oSettings.aoColumns[iCol].bVisible == bShow) {
				return;
			}

			var nTrHead = $('>tr', oSettings.nTHead)[0];
			var nTrFoot = $('>tr', oSettings.nTFoot)[0];
			var anTheadTh = [];
			var anTfootTh = [];
			for (i = 0; i < iColumns; i++) {
				anTheadTh.push(oSettings.aoColumns[i].nTh);
				anTfootTh.push(oSettings.aoColumns[i].nTf);
			}

			/* Show the column */
			if (bShow) {
				var iInsert = 0;
				for (i = 0; i < iCol; i++) {
					if (oSettings.aoColumns[i].bVisible) {
						iInsert++;
					}
				}

				/* Need to decide if we should use appendChild or insertBefore */
				if (iInsert >= _fnVisbleColumns(oSettings)) {
					nTrHead.appendChild(anTheadTh[iCol]);
					if (nTrFoot) {
						nTrFoot.appendChild(anTfootTh[iCol]);
					}

					for (i = 0, iLen = oSettings.aoData.length; i < iLen; i++) {
						nTd = oSettings.aoData[i]._anHidden[iCol];
						oSettings.aoData[i].nTr.appendChild(nTd);
					}
				}
				else {
					/* Which coloumn should we be inserting before? */
					var iBefore;
					for (i = iCol; i < iColumns; i++) {
						iBefore = _fnColumnIndexToVisible(oSettings, i);
						if (iBefore !== null) {
							break;
						}
					}

					nTrHead.insertBefore(anTheadTh[iCol], nTrHead.getElementsByTagName('th')[iBefore]);
					if (nTrFoot) {
						nTrFoot.insertBefore(anTfootTh[iCol], nTrFoot.getElementsByTagName('th')[iBefore]);
					}

					anTds = _fnGetTdNodes(oSettings);
					for (i = 0, iLen = oSettings.aoData.length; i < iLen; i++) {
						nTd = oSettings.aoData[i]._anHidden[iCol];
						oSettings.aoData[i].nTr.insertBefore(nTd, $('>td:eq(' + iBefore + ')',
							oSettings.aoData[i].nTr)[0]);
					}
				}

				oSettings.aoColumns[iCol].bVisible = true;
			}
			else {
				/* Remove a column from display */
				nTrHead.removeChild(anTheadTh[iCol]);
				if (nTrFoot) {
					nTrFoot.removeChild(anTfootTh[iCol]);
				}

				anTds = _fnGetTdNodes(oSettings);
				for (i = 0, iLen = oSettings.aoData.length; i < iLen; i++) {
					nTd = anTds[(i * oSettings.aoColumns.length) + (iCol * 1)];
					oSettings.aoData[i]._anHidden[iCol] = nTd;
					nTd.parentNode.removeChild(nTd);
				}

				oSettings.aoColumns[iCol].bVisible = false;
			}

			/* If there are any 'open' rows, then we need to alter the colspan for this col change */
			for (i = 0, iLen = oSettings.aoOpenRows.length; i < iLen; i++) {
				oSettings.aoOpenRows[i].nTr.colSpan = _fnVisbleColumns(oSettings);
			}

			/* Do a redraw incase anything depending on the table columns needs it 
			 * (built-in: scrolling) 
			 */
			_fnAjustColumnSizing(oSettings);
			_fnDraw(oSettings);
		};

		/*
		 * Function: fnPageChange
		 * Purpose:  Change the pagination
		 * Returns:  -
		 * Inputs:   string:sAction - paging action to take: "first", "previous", "next" or "last"
		 *           bool:bRedraw - redraw the table or not - optional - default true
		 */
		this.fnPageChange = function (sAction, bRedraw) {
			var oSettings = _fnSettingsFromNode(this[_oExt.iApiIndex]);
			_fnPageChange(oSettings, sAction);
			_fnCalculateEnd(oSettings);

			if (typeof bRedraw == 'undefined' || bRedraw) {
				_fnDraw(oSettings);
			}
		};

		/*
		 * Function: fnDestroy
		 * Purpose:  Destructor for the DataTable
		 * Returns:  -
		 * Inputs:   -
		 */
		this.fnDestroy = function () {
			var oSettings = _fnSettingsFromNode(this[_oExt.iApiIndex]);
			var nOrig = oSettings.nTableWrapper.parentNode;
			var nBody = oSettings.nTBody;
			var i, iLen;

			/* Remove the DataTables generated nodes, events and classes */
			oSettings.nTable.parentNode.removeChild(oSettings.nTable);
			$(oSettings.nTableWrapper).remove();

			oSettings.aaSorting = [];
			oSettings.aaSortingFixed = [];
			_fnSortingClasses(oSettings);

			$(_fnGetTrNodes(oSettings)).removeClass(oSettings.asStripClasses.join(' '));

			if (!oSettings.bJUI) {
				$('th', oSettings.nTHead).removeClass([_oExt.oStdClasses.sSortable,
				_oExt.oStdClasses.sSortableAsc,
				_oExt.oStdClasses.sSortableDesc,
				_oExt.oStdClasses.sSortableNone].join(' ')
				);
			}
			else {
				$('th', oSettings.nTHead).removeClass([_oExt.oStdClasses.sSortable,
				_oExt.oJUIClasses.sSortableAsc,
				_oExt.oJUIClasses.sSortableDesc,
				_oExt.oJUIClasses.sSortableNone].join(' ')
				);
				$('th span', oSettings.nTHead).remove();
			}

			/* Add the TR elements back into the table in their original order */
			nOrig.appendChild(oSettings.nTable);
			for (i = 0, iLen = oSettings.aoData.length; i < iLen; i++) {
				nBody.appendChild(oSettings.aoData[i].nTr);
			}

			/* If the were originally odd/even type classes - then we add them back here. Note
			 * this is not fool proof (for example if not all rows as odd/even classes - but 
			 * it's a good effort without getting carried away
			 */
			$('>tr:even', nBody).addClass(oSettings.asDestoryStrips[0]);
			$('>tr:odd', nBody).addClass(oSettings.asDestoryStrips[1]);

			/* Remove the settings object from the settings array */
			for (i = 0, iLen = _aoSettings.length; i < iLen; i++) {
				if (_aoSettings[i] == oSettings) {
					_aoSettings.splice(i, 1);
				}
			}

			/* End it all */
			oSettings = null;
		};

		/*
		 * Function: _fnAjustColumnSizing
		 * Purpose:  Update tale sizing based on content. This would most likely be used for scrolling
		 *   and will typically need a redraw after it.
		 * Returns:  -
		 * Inputs:   bool:bRedraw - redraw the table or not, you will typically want to - default true
		 */
		this.fnAdjustColumnSizing = function (bRedraw) {
			_fnAjustColumnSizing(_fnSettingsFromNode(this[_oExt.iApiIndex]));

			if (typeof bRedraw == 'undefined' || bRedraw) {
				this.fnDraw(false);
			}
		};

		/*
		 * Plugin API functions
		 * 
		 * This call will add the functions which are defined in _oExt.oApi to the
		 * DataTables object, providing a rather nice way to allow plug-in API functions. Note that
		 * this is done here, so that API function can actually override the built in API functions if
		 * required for a particular purpose.
		 */

		/*
		 * Function: _fnExternApiFunc
		 * Purpose:  Create a wrapper function for exporting an internal func to an external API func
		 * Returns:  function: - wrapped function
		 * Inputs:   string:sFunc - API function name
		 */
		function _fnExternApiFunc(sFunc) {
			return function () {
				var aArgs = [_fnSettingsFromNode(this[_oExt.iApiIndex])].concat(
					Array.prototype.slice.call(arguments));
				return _oExt.oApi[sFunc].apply(this, aArgs);
			};
		}

		for (var sFunc in _oExt.oApi) {
			if (sFunc) {
				/*
				 * Function: anon
				 * Purpose:  Wrap the plug-in API functions in order to provide the settings as 1st arg 
				 *   and execute in this scope
				 * Returns:  -
				 * Inputs:   -
				 */
				this[sFunc] = _fnExternApiFunc(sFunc);
			}
		}



		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - Local functions
		 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - Initalisation
		 */

		/*
		 * Function: _fnInitalise
		 * Purpose:  Draw the table for the first time, adding all required features
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnInitalise(oSettings) {
			/* Ensure that the table data is fully initialised */
			if (oSettings.bInitialised === false) {
				setTimeout(function () { _fnInitalise(oSettings); }, 200);
				return;
			}

			/* Show the display HTML options */
			_fnAddOptionsHtml(oSettings);

			/* Draw the headers for the table */
			_fnDrawHead(oSettings);

			/* If there is default sorting required - let's do it. The sort function
			 * will do the drawing for us. Otherwise we draw the table
			 */
			if (oSettings.oFeatures.bSort) {
				_fnSort(oSettings);
			}
			else {
				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
				_fnCalculateEnd(oSettings);
				_fnDraw(oSettings);
			}

			/* if there is an ajax source */
			if (oSettings.sAjaxSource !== null && !oSettings.oFeatures.bServerSide) {
				_fnProcessingDisplay(oSettings, true);

				oSettings.fnServerData.call(oSettings.oInstance, oSettings.sAjaxSource, null, function (json) {
					/* Got the data - add it to the table */
					for (var i = 0; i < json.aaData.length; i++) {
						_fnAddData(oSettings, json.aaData[i]);
					}

					/* Reset the init display for cookie saving. We've already done a filter, and
					 * therefore cleared it before. So we need to make it appear 'fresh'
					 */
					oSettings.iInitDisplayStart = oSettings._iDisplayStart;

					if (oSettings.oFeatures.bSort) {
						_fnSort(oSettings);
					}
					else {
						oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
						_fnCalculateEnd(oSettings);
						_fnDraw(oSettings);
					}

					_fnProcessingDisplay(oSettings, false);

					/* Run the init callback if there is one */
					if (typeof oSettings.fnInitComplete == 'function') {
						oSettings.fnInitComplete.call(oSettings.oInstance, oSettings, json);
					}
				});
				return;
			}

			/* Run the init callback if there is one */
			if (typeof oSettings.fnInitComplete == 'function') {
				oSettings.fnInitComplete.call(oSettings.oInstance, oSettings);
			}

			if (!oSettings.oFeatures.bServerSide) {
				_fnProcessingDisplay(oSettings, false);
			}
		}

		/*
		 * Function: _fnLanguageProcess
		 * Purpose:  Copy language variables from remote object to a local one
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 *           object:oLanguage - Language information
		 *           bool:bInit - init once complete
		 */
		function _fnLanguageProcess(oSettings, oLanguage, bInit) {
			_fnMap(oSettings.oLanguage, oLanguage, 'sProcessing');
			_fnMap(oSettings.oLanguage, oLanguage, 'sLengthMenu');
			_fnMap(oSettings.oLanguage, oLanguage, 'sEmptyTable');
			_fnMap(oSettings.oLanguage, oLanguage, 'sZeroRecords');
			_fnMap(oSettings.oLanguage, oLanguage, 'sInfo');
			_fnMap(oSettings.oLanguage, oLanguage, 'sInfoEmpty');
			_fnMap(oSettings.oLanguage, oLanguage, 'sInfoFiltered');
			_fnMap(oSettings.oLanguage, oLanguage, 'sInfoPostFix');
			_fnMap(oSettings.oLanguage, oLanguage, 'sSearch');

			if (typeof oLanguage.oPaginate != 'undefined') {
				_fnMap(oSettings.oLanguage.oPaginate, oLanguage.oPaginate, 'sFirst');
				_fnMap(oSettings.oLanguage.oPaginate, oLanguage.oPaginate, 'sPrevious');
				_fnMap(oSettings.oLanguage.oPaginate, oLanguage.oPaginate, 'sNext');
				_fnMap(oSettings.oLanguage.oPaginate, oLanguage.oPaginate, 'sLast');
			}

			/* Backwards compatibility - if there is no sEmptyTable given, then use the same as
			 * sZeroRecords - assuming that is given.
			 */
			if (typeof oLanguage.sEmptyTable == 'undefined' &&
				typeof oLanguage.sZeroRecords != 'undefined') {
				_fnMap(oSettings.oLanguage, oLanguage, 'sZeroRecords', 'sEmptyTable');
			}

			if (bInit) {
				_fnInitalise(oSettings);
			}
		}

		/*
		 * Function: _fnAddColumn
		 * Purpose:  Add a column to the list used for the table with default values
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 *           node:nTh - the th element for this column
		 */
		function _fnAddColumn(oSettings, nTh) {
			oSettings.aoColumns[oSettings.aoColumns.length++] = {
				"sType": null,
				"_bAutoType": true,
				"bVisible": true,
				"bSearchable": true,
				"bSortable": true,
				"asSorting": ['asc', 'desc'],
				"sSortingClass": oSettings.oClasses.sSortable,
				"sSortingClassJUI": oSettings.oClasses.sSortJUI,
				"sTitle": nTh ? nTh.innerHTML : '',
				"sName": '',
				"sWidth": null,
				"sWidthOrig": null,
				"sClass": null,
				"fnRender": null,
				"bUseRendered": true,
				"iDataSort": oSettings.aoColumns.length - 1,
				"sSortDataType": 'std',
				"nTh": nTh ? nTh : document.createElement('th'),
				"nTf": null
			};

			var iCol = oSettings.aoColumns.length - 1;
			var oCol = oSettings.aoColumns[iCol];

			/* Add a column specific filter */
			if (typeof oSettings.aoPreSearchCols[iCol] == 'undefined' ||
				oSettings.aoPreSearchCols[iCol] === null) {
				oSettings.aoPreSearchCols[iCol] = {
					"sSearch": "",
					"bRegex": false,
					"bSmart": true
				};
			}
			else {
				/* Don't require that the user must specify bRegex and / or bSmart */
				if (typeof oSettings.aoPreSearchCols[iCol].bRegex == 'undefined') {
					oSettings.aoPreSearchCols[iCol].bRegex = true;
				}

				if (typeof oSettings.aoPreSearchCols[iCol].bSmart == 'undefined') {
					oSettings.aoPreSearchCols[iCol].bSmart = true;
				}
			}

			/* Use the column options function to initialise classes etc */
			_fnColumnOptions(oSettings, iCol, null);
		}

		/*
		 * Function: _fnColumnOptions
		 * Purpose:  Apply options for a column
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 *           int:iCol - column index to consider
		 *           object:oOptions - object with sType, bVisible and bSearchable
		 */
		function _fnColumnOptions(oSettings, iCol, oOptions) {
			var oCol = oSettings.aoColumns[iCol];

			/* User specified column options */
			if (typeof oOptions != 'undefined' && oOptions !== null) {
				if (typeof oOptions.sType != 'undefined') {
					oCol.sType = oOptions.sType;
					oCol._bAutoType = false;
				}

				_fnMap(oCol, oOptions, "bVisible");
				_fnMap(oCol, oOptions, "bSearchable");
				_fnMap(oCol, oOptions, "bSortable");
				_fnMap(oCol, oOptions, "sTitle");
				_fnMap(oCol, oOptions, "sName");
				_fnMap(oCol, oOptions, "sWidth");
				_fnMap(oCol, oOptions, "sWidth", "sWidthOrig");
				_fnMap(oCol, oOptions, "sClass");
				_fnMap(oCol, oOptions, "fnRender");
				_fnMap(oCol, oOptions, "bUseRendered");
				_fnMap(oCol, oOptions, "iDataSort");
				_fnMap(oCol, oOptions, "asSorting");
				_fnMap(oCol, oOptions, "sSortDataType");
			}

			/* Feature sorting overrides column specific when off */
			if (!oSettings.oFeatures.bSort) {
				oCol.bSortable = false;
			}

			/* Check that the class assignment is correct for sorting */
			if (!oCol.bSortable ||
				($.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) == -1)) {
				oCol.sSortingClass = oSettings.oClasses.sSortableNone;
				oCol.sSortingClassJUI = "";
			}
			else if ($.inArray('asc', oCol.asSorting) != -1 && $.inArray('desc', oCol.asSorting) == -1) {
				oCol.sSortingClass = oSettings.oClasses.sSortableAsc;
				oCol.sSortingClassJUI = oSettings.oClasses.sSortJUIAscAllowed;
			}
			else if ($.inArray('asc', oCol.asSorting) == -1 && $.inArray('desc', oCol.asSorting) != -1) {
				oCol.sSortingClass = oSettings.oClasses.sSortableDesc;
				oCol.sSortingClassJUI = oSettings.oClasses.sSortJUIDescAllowed;
			}
		}

		/*
		 * Function: _fnAddData
		 * Purpose:  Add a data array to the table, creating DOM node etc
		 * Returns:  int: - >=0 if successful (index of new aoData entry), -1 if failed
		 * Inputs:   object:oSettings - dataTables settings object
		 *           array:aData - data array to be added
		 * Notes:    There are two basic methods for DataTables to get data to display - a JS array
		 *   (which is dealt with by this function), and the DOM, which has it's own optimised
		 *   function (_fnGatherData). Be careful to make the same changes here as there and vice-versa
		 */
		function _fnAddData(oSettings, aDataSupplied) {
			/* Sanity check the length of the new array */
			if (aDataSupplied.length != oSettings.aoColumns.length &&
				oSettings.iDrawError != oSettings.iDraw) {
				_fnLog(oSettings, 0, "Added data does not match known number of columns");
				oSettings.iDrawError = oSettings.iDraw;
				return -1;
			}


			/* Create the object for storing information about this new row */
			var aData = aDataSupplied.slice();
			var iThisIndex = oSettings.aoData.length;
			oSettings.aoData.push({
				"nTr": document.createElement('tr'),
				"_iId": oSettings.iNextId++,
				"_aData": aData,
				"_anHidden": [],
				"_sRowStripe": ''
			});

			/* Create the cells */
			var nTd, sThisType;
			for (var i = 0; i < aData.length; i++) {
				nTd = document.createElement('td');

				/* Allow null data (from a data array) - simply deal with it as a blank string */
				if (aData[i] === null) {
					aData[i] = '';
				}

				/* Cast everything as a string - this allows us to treat everything accurately in the
				 * sorting functions
				 */
				if (typeof aData[i] != 'string') {
					aData[i] += "";
				}
				aData[i] = $.trim(aData[i]);

				if (typeof oSettings.aoColumns[i].fnRender == 'function') {
					var sRendered = oSettings.aoColumns[i].fnRender({
						"iDataRow": iThisIndex,
						"iDataColumn": i,
						"aData": aData,
						"oSettings": oSettings
					});
					nTd.innerHTML = sRendered;
					if (oSettings.aoColumns[i].bUseRendered) {
						/* Use the rendered data for filtering/sorting */
						oSettings.aoData[iThisIndex]._aData[i] = sRendered;
					}
				}
				else {
					nTd.innerHTML = aData[i];
				}

				if (oSettings.aoColumns[i].sClass !== null) {
					nTd.className = oSettings.aoColumns[i].sClass;
				}

				/* See if we should auto-detect the column type */
				if (oSettings.aoColumns[i]._bAutoType && oSettings.aoColumns[i].sType != 'string') {
					/* Attempt to auto detect the type - same as _fnGatherData() */
					sThisType = _fnDetectType(oSettings.aoData[iThisIndex]._aData[i]);
					if (oSettings.aoColumns[i].sType === null) {
						oSettings.aoColumns[i].sType = sThisType;
					}
					else if (oSettings.aoColumns[i].sType != sThisType) {
						/* String is always the 'fallback' option */
						oSettings.aoColumns[i].sType = 'string';
					}
				}

				if (oSettings.aoColumns[i].bVisible) {
					oSettings.aoData[iThisIndex].nTr.appendChild(nTd);
				}
				else {
					oSettings.aoData[iThisIndex]._anHidden[i] = nTd;
				}
			}

			/* Add to the display array */
			oSettings.aiDisplayMaster.push(iThisIndex);
			return iThisIndex;
		}

		/*
		 * Function: _fnGatherData
		 * Purpose:  Read in the data from the target table from the DOM
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 * Notes:    This is a optimised version of _fnAddData (more or less) for reading information
		 *   from the DOM. The basic actions must be identical in the two functions.
		 */
		function _fnGatherData(oSettings) {
			var iLoop, i, iLen, j, jLen, jInner,
				nTds, nTrs, nTd, aLocalData, iThisIndex,
				iRow, iRows, iColumn, iColumns;

			/*
			 * Process by row first
			 * Add the data object for the whole table - storing the tr node. Note - no point in getting
			 * DOM based data if we are going to go and replace it with Ajax source data.
			 */
			if (oSettings.sAjaxSource === null) {
				nTrs = oSettings.nTBody.childNodes;
				for (i = 0, iLen = nTrs.length; i < iLen; i++) {
					if (nTrs[i].nodeName.toUpperCase() == "TR") {
						iThisIndex = oSettings.aoData.length;
						oSettings.aoData.push({
							"nTr": nTrs[i],
							"_iId": oSettings.iNextId++,
							"_aData": [],
							"_anHidden": [],
							"_sRowStripe": ''
						});

						oSettings.aiDisplayMaster.push(iThisIndex);

						aLocalData = oSettings.aoData[iThisIndex]._aData;
						nTds = nTrs[i].childNodes;
						jInner = 0;

						for (j = 0, jLen = nTds.length; j < jLen; j++) {
							if (nTds[j].nodeName.toUpperCase() == "TD") {
								aLocalData[jInner] = $.trim(nTds[j].innerHTML);
								jInner++;
							}
						}
					}
				}
			}

			/* Gather in the TD elements of the Table - note that this is basically the same as
			 * fnGetTdNodes, but that function takes account of hidden columns, which we haven't yet
			 * setup!
			 */
			nTrs = _fnGetTrNodes(oSettings);
			nTds = [];
			for (i = 0, iLen = nTrs.length; i < iLen; i++) {
				for (j = 0, jLen = nTrs[i].childNodes.length; j < jLen; j++) {
					nTd = nTrs[i].childNodes[j];
					if (nTd.nodeName.toUpperCase() == "TD") {
						nTds.push(nTd);
					}
				}
			}

			/* Sanity check */
			if (nTds.length != nTrs.length * oSettings.aoColumns.length) {
				_fnLog(oSettings, 1, "Unexpected number of TD elements. Expected " +
					(nTrs.length * oSettings.aoColumns.length) + " and got " + nTds.length + ". DataTables does " +
					"not support rowspan / colspan in the table body, and there must be one cell for each " +
					"row/column combination.");
			}

			/* Now process by column */
			for (iColumn = 0, iColumns = oSettings.aoColumns.length; iColumn < iColumns; iColumn++) {
				/* Get the title of the column - unless there is a user set one */
				if (oSettings.aoColumns[iColumn].sTitle === null) {
					oSettings.aoColumns[iColumn].sTitle = oSettings.aoColumns[iColumn].nTh.innerHTML;
				}

				var
					bAutoType = oSettings.aoColumns[iColumn]._bAutoType,
					bRender = typeof oSettings.aoColumns[iColumn].fnRender == 'function',
					bClass = oSettings.aoColumns[iColumn].sClass !== null,
					bVisible = oSettings.aoColumns[iColumn].bVisible,
					nCell, sThisType, sRendered;

				/* A single loop to rule them all (and be more efficient) */
				if (bAutoType || bRender || bClass || !bVisible) {
					for (iRow = 0, iRows = oSettings.aoData.length; iRow < iRows; iRow++) {
						nCell = nTds[(iRow * iColumns) + iColumn];

						/* Type detection */
						if (bAutoType) {
							if (oSettings.aoColumns[iColumn].sType != 'string') {
								sThisType = _fnDetectType(oSettings.aoData[iRow]._aData[iColumn]);
								if (oSettings.aoColumns[iColumn].sType === null) {
									oSettings.aoColumns[iColumn].sType = sThisType;
								}
								else if (oSettings.aoColumns[iColumn].sType != sThisType) {
									/* String is always the 'fallback' option */
									oSettings.aoColumns[iColumn].sType = 'string';
								}
							}
						}

						/* Rendering */
						if (bRender) {
							sRendered = oSettings.aoColumns[iColumn].fnRender({
								"iDataRow": iRow,
								"iDataColumn": iColumn,
								"aData": oSettings.aoData[iRow]._aData,
								"oSettings": oSettings
							});
							nCell.innerHTML = sRendered;
							if (oSettings.aoColumns[iColumn].bUseRendered) {
								/* Use the rendered data for filtering/sorting */
								oSettings.aoData[iRow]._aData[iColumn] = sRendered;
							}
						}

						/* Classes */
						if (bClass) {
							nCell.className += ' ' + oSettings.aoColumns[iColumn].sClass;
						}

						/* Column visability */
						if (!bVisible) {
							oSettings.aoData[iRow]._anHidden[iColumn] = nCell;
							nCell.parentNode.removeChild(nCell);
						}
					}
				}
			}
		}



		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - Drawing functions
		 */

		/*
		 * Function: _fnDrawHead
		 * Purpose:  Create the HTML header for the table
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnDrawHead(oSettings) {
			var i, nTh, iLen;
			var iThs = oSettings.nTHead.getElementsByTagName('th').length;
			var iCorrector = 0;

			/* If there is a header in place - then use it - otherwise it's going to get nuked... */
			if (iThs !== 0) {
				/* We've got a thead from the DOM, so remove hidden columns and apply width to vis cols */
				for (i = 0, iLen = oSettings.aoColumns.length; i < iLen; i++) {
					nTh = oSettings.aoColumns[i].nTh;

					if (oSettings.aoColumns[i].bVisible) {
						/* Set width */
						if (oSettings.aoColumns[i].sWidth !== null) {
							nTh.style.width = oSettings.aoColumns[i].sWidth;
						}

						/* Set the title of the column if it is user defined (not what was auto detected) */
						if (oSettings.aoColumns[i].sTitle != nTh.innerHTML) {
							nTh.innerHTML = oSettings.aoColumns[i].sTitle;
						}
					}
					else {
						nTh.parentNode.removeChild(nTh);
						iCorrector++;
					}
				}
			}
			else {
				/* We don't have a header in the DOM - so we are going to have to create one */
				var nTr = document.createElement("tr");

				for (i = 0, iLen = oSettings.aoColumns.length; i < iLen; i++) {
					nTh = oSettings.aoColumns[i].nTh;
					nTh.innerHTML = oSettings.aoColumns[i].sTitle;

					if (oSettings.aoColumns[i].bVisible) {
						if (oSettings.aoColumns[i].sClass !== null) {
							nTh.className = oSettings.aoColumns[i].sClass;
						}

						if (oSettings.aoColumns[i].sWidth !== null) {
							nTh.style.width = oSettings.aoColumns[i].sWidth;
						}

						nTr.appendChild(nTh);
					}
				}
				$(oSettings.nTHead).html('')[0].appendChild(nTr);
			}

			/* Add the extra markup needed by jQuery UI's themes */
			if (oSettings.bJUI) {
				for (i = 0, iLen = oSettings.aoColumns.length; i < iLen; i++) {
					oSettings.aoColumns[i].nTh.insertBefore(document.createElement('span'),
						oSettings.aoColumns[i].nTh.firstChild);
				}
			}

			/* Add sort listener */
			if (oSettings.oFeatures.bSort) {
				for (i = 0; i < oSettings.aoColumns.length; i++) {
					if (oSettings.aoColumns[i].bSortable !== false) {
						_fnSortAttachListener(oSettings, oSettings.aoColumns[i].nTh, i);
					}
					else {
						$(oSettings.aoColumns[i].nTh).addClass(oSettings.oClasses.sSortableNone);
					}
				}

				/* Take the brutal approach to cancelling text selection in header */
				$('th', oSettings.nTHead).mousedown(function (e) {
					this.onselectstart = function () { return false; };
					return false;
				});
			}

			/* Cache the footer elements */
			if (oSettings.nTFoot !== null) {
				iCorrector = 0;
				var nTfs = oSettings.nTFoot.getElementsByTagName('th');
				for (i = 0, iLen = nTfs.length; i < iLen; i++) {
					if (typeof oSettings.aoColumns[i] != 'undefined') {
						oSettings.aoColumns[i].nTf = nTfs[i - iCorrector];

						if (oSettings.oClasses.sFooterTH !== "") {
							oSettings.aoColumns[i].nTf.className += " " + oSettings.oClasses.sFooterTH;
						}

						if (!oSettings.aoColumns[i].bVisible) {
							nTfs[i - iCorrector].parentNode.removeChild(nTfs[i - iCorrector]);
							iCorrector++;
						}
					}
				}
			}
		}

		/*
		 * Function: _fnDraw
		 * Purpose:  Insert the required TR nodes into the table for display
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnDraw(oSettings) {
			var i, iLen;
			var anRows = [];
			var iRowCount = 0;
			var bRowError = false;
			var iStrips = oSettings.asStripClasses.length;
			var iOpenRows = oSettings.aoOpenRows.length;

			/* Check and see if we have an initial draw position from state saving */
			if (typeof oSettings.iInitDisplayStart != 'undefined' && oSettings.iInitDisplayStart != -1) {
				if (oSettings.oFeatures.bServerSide) {
					oSettings._iDisplayStart = oSettings.iInitDisplayStart;
				}
				else {
					oSettings._iDisplayStart = (oSettings.iInitDisplayStart >= oSettings.fnRecordsDisplay()) ?
						0 : oSettings.iInitDisplayStart;
				}
				oSettings.iInitDisplayStart = -1;
				_fnCalculateEnd(oSettings);
			}

			/* If we are dealing with Ajax - do it here */
			if (oSettings.oFeatures.bServerSide &&
				!_fnAjaxUpdate(oSettings)) {
				return;
			}

			if (oSettings.aiDisplay.length !== 0) {
				var iStart = oSettings._iDisplayStart;
				var iEnd = oSettings._iDisplayEnd;

				if (oSettings.oFeatures.bServerSide) {
					iStart = 0;
					iEnd = oSettings.aoData.length;
				}

				for (var j = iStart; j < iEnd; j++) {
					var aoData = oSettings.aoData[oSettings.aiDisplay[j]];
					var nRow = aoData.nTr;

					/* Remove the old stripping classes and then add the new one */
					if (iStrips !== 0) {
						var sStrip = oSettings.asStripClasses[iRowCount % iStrips];
						if (aoData._sRowStripe != sStrip) {
							$(nRow).removeClass(aoData._sRowStripe).addClass(sStrip);
							aoData._sRowStripe = sStrip;
						}
					}

					/* Custom row callback function - might want to manipule the row */
					if (typeof oSettings.fnRowCallback == "function") {
						nRow = oSettings.fnRowCallback.call(oSettings.oInstance, nRow,
							oSettings.aoData[oSettings.aiDisplay[j]]._aData, iRowCount, j);
						if (!nRow && !bRowError) {
							_fnLog(oSettings, 0, "A node was not returned by fnRowCallback");
							bRowError = true;
						}
					}

					anRows.push(nRow);
					iRowCount++;

					/* If there is an open row - and it is attached to this parent - attach it on redraw */
					if (iOpenRows !== 0) {
						for (var k = 0; k < iOpenRows; k++) {
							if (nRow == oSettings.aoOpenRows[k].nParent) {
								anRows.push(oSettings.aoOpenRows[k].nTr);
							}
						}
					}
				}
			}
			else {
				/* Table is empty - create a row with an empty message in it */
				anRows[0] = document.createElement('tr');

				if (typeof oSettings.asStripClasses[0] != 'undefined') {
					anRows[0].className = oSettings.asStripClasses[0];
				}

				var nTd = document.createElement('td');
				nTd.setAttribute('valign', "top");
				nTd.colSpan = _fnVisbleColumns(oSettings);
				nTd.className = oSettings.oClasses.sRowEmpty;
				if (typeof oSettings.oLanguage.sEmptyTable != 'undefined' &&
					oSettings.fnRecordsTotal() === 0) {
					nTd.innerHTML = oSettings.oLanguage.sEmptyTable;
				}
				else {
					nTd.innerHTML = oSettings.oLanguage.sZeroRecords.replace(
						'_MAX_', oSettings.fnFormatNumber(oSettings.fnRecordsTotal()));
				}

				anRows[iRowCount].appendChild(nTd);
			}

			/* Callback the header and footer custom funcation if there is one */
			if (typeof oSettings.fnHeaderCallback == 'function') {
				oSettings.fnHeaderCallback.call(oSettings.oInstance, $('>tr', oSettings.nTHead)[0],
					_fnGetDataMaster(oSettings), oSettings._iDisplayStart, oSettings.fnDisplayEnd(),
					oSettings.aiDisplay);
			}

			if (typeof oSettings.fnFooterCallback == 'function') {
				oSettings.fnFooterCallback.call(oSettings.oInstance, $('>tr', oSettings.nTFoot)[0],
					_fnGetDataMaster(oSettings), oSettings._iDisplayStart, oSettings.fnDisplayEnd(),
					oSettings.aiDisplay);
			}

			/* 
			 * Need to remove any old row from the display - note we can't just empty the tbody using
			 * $().html('') since this will unbind the jQuery event handlers (even although the node 
			 * still exists!) - equally we can't use innerHTML, since IE throws an exception.
			 */
			var
				nAddFrag = document.createDocumentFragment(),
				nRemoveFrag = document.createDocumentFragment(),
				nBodyPar, nTrs;

			if (oSettings.nTBody) {
				nBodyPar = oSettings.nTBody.parentNode;
				nRemoveFrag.appendChild(oSettings.nTBody);

				nTrs = oSettings.nTBody.childNodes;
				for (i = nTrs.length - 1; i >= 0; i--) {
					nTrs[i].parentNode.removeChild(nTrs[i]);
				}

				/* Put the draw table into the dom */
				for (i = 0, iLen = anRows.length; i < iLen; i++) {
					nAddFrag.appendChild(anRows[i]);
				}

				oSettings.nTBody.appendChild(nAddFrag);
				if (nBodyPar !== null) {
					nBodyPar.appendChild(oSettings.nTBody);
				}
			}

			/* Perform certain DOM operations after the table has been drawn for the first time */
			if (typeof oSettings._bInitComplete == "undefined") {
				oSettings._bInitComplete = true;

				/* It is possible that some of the DOM created (particularly if custom) has padding etc
				 * on it which means that the table size is more constrained that when we originally
				 * measured it. As such we check here if this is the case, and correct if needed
				 */
				if (oSettings.nTableWrapper != oSettings.nTable.parentNode &&
					$(oSettings.nTableWrapper).width() > $(oSettings.nTable.parentNode).width()) {
					_fnAjustColumnSizing(oSettings);
				}

				/* Set an absolute width for the table such that pagination doesn't
				 * cause the table to resize - disabled for now.
				 */
				//if ( oSettings.oFeatures.bAutoWidth && oSettings.nTable.offsetWidth !== 0 )
				//{
				//	//oSettings.nTable.style.width = oSettings.nTable.offsetWidth+"px";
				//}
			}

			/* Call all required callback functions for the end of a draw */
			for (i = 0, iLen = oSettings.aoDrawCallback.length; i < iLen; i++) {
				oSettings.aoDrawCallback[i].fn.call(oSettings.oInstance, oSettings);
			}

			/* Draw is complete, sorting and filtering must be as well */
			oSettings.bSorted = false;
			oSettings.bFiltered = false;
		}

		/*
		 * Function: _fnReDraw
		 * Purpose:  Redraw the table - taking account of the various features which are enabled
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnReDraw(oSettings) {
			if (oSettings.oFeatures.bSort) {
				/* Sorting will refilter and draw for us */
				_fnSort(oSettings, oSettings.oPreviousSearch);
			}
			else if (oSettings.oFeatures.bFilter) {
				/* Filtering will redraw for us */
				_fnFilterComplete(oSettings, oSettings.oPreviousSearch);
			}
			else {
				_fnCalculateEnd(oSettings);
				_fnDraw(oSettings);
			}
		}

		/*
		 * Function: _fnAjaxUpdate
		 * Purpose:  Update the table using an Ajax call
		 * Returns:  bool: block the table drawing or not
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnAjaxUpdate(oSettings) {
			if (oSettings.bAjaxDataGet) {
				_fnProcessingDisplay(oSettings, true);
				var iColumns = oSettings.aoColumns.length;
				var aoData = [];
				var i;

				/* Paging and general */
				oSettings.iDraw++;
				aoData.push({ "name": "sEcho", "value": oSettings.iDraw });
				aoData.push({ "name": "iColumns", "value": iColumns });
				aoData.push({ "name": "sColumns", "value": _fnColumnOrdering(oSettings) });
				aoData.push({ "name": "iDisplayStart", "value": oSettings._iDisplayStart });
				aoData.push({
					"name": "iDisplayLength", "value": oSettings.oFeatures.bPaginate !== false ?
						oSettings._iDisplayLength : -1
				});

				/* Filtering */
				if (oSettings.oFeatures.bFilter !== false) {
					aoData.push({ "name": "sSearch", "value": oSettings.oPreviousSearch.sSearch });
					aoData.push({ "name": "bRegex", "value": oSettings.oPreviousSearch.bRegex });
					for (i = 0; i < iColumns; i++) {
						aoData.push({ "name": "sSearch_" + i, "value": oSettings.aoPreSearchCols[i].sSearch });
						aoData.push({ "name": "bRegex_" + i, "value": oSettings.aoPreSearchCols[i].bRegex });
						aoData.push({ "name": "bSearchable_" + i, "value": oSettings.aoColumns[i].bSearchable });
					}
				}

				/* Sorting */
				if (oSettings.oFeatures.bSort !== false) {
					var iFixed = oSettings.aaSortingFixed !== null ? oSettings.aaSortingFixed.length : 0;
					var iUser = oSettings.aaSorting.length;
					aoData.push({ "name": "iSortingCols", "value": iFixed + iUser });
					for (i = 0; i < iFixed; i++) {
						aoData.push({ "name": "iSortCol_" + i, "value": oSettings.aaSortingFixed[i][0] });
						aoData.push({ "name": "sSortDir_" + i, "value": oSettings.aaSortingFixed[i][1] });
					}

					for (i = 0; i < iUser; i++) {
						aoData.push({ "name": "iSortCol_" + (i + iFixed), "value": oSettings.aaSorting[i][0] });
						aoData.push({ "name": "sSortDir_" + (i + iFixed), "value": oSettings.aaSorting[i][1] });
					}

					for (i = 0; i < iColumns; i++) {
						aoData.push({ "name": "bSortable_" + i, "value": oSettings.aoColumns[i].bSortable });
					}
				}

				oSettings.fnServerData.call(oSettings.oInstance, oSettings.sAjaxSource, aoData,
					function (json) {
						_fnAjaxUpdateDraw(oSettings, json);
					});
				return false;
			}
			else {
				return true;
			}
		}

		/*
		 * Function: _fnAjaxUpdateDraw
		 * Purpose:  Data the data from the server (nuking the old) and redraw the table
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 *           object:json - json data return from the server.
		 *             The following must be defined:
		 *               iTotalRecords, iTotalDisplayRecords, aaData
		 *             The following may be defined:
		 *               sColumns
		 */
		function _fnAjaxUpdateDraw(oSettings, json) {
			if (typeof json.sEcho != 'undefined') {
				/* Protect against old returns over-writing a new one. Possible when you get
				 * very fast interaction, and later queires are completed much faster
				 */
				if (json.sEcho * 1 < oSettings.iDraw) {
					return;
				}
				else {
					oSettings.iDraw = json.sEcho * 1;
				}
			}

			_fnClearTable(oSettings);
			oSettings._iRecordsTotal = json.iTotalRecords;
			oSettings._iRecordsDisplay = json.iTotalDisplayRecords;

			/* Determine if reordering is required */
			var sOrdering = _fnColumnOrdering(oSettings);
			var bReOrder = (typeof json.sColumns != 'undefined' && sOrdering !== "" && json.sColumns != sOrdering);
			if (bReOrder) {
				var aiIndex = _fnReOrderIndex(oSettings, json.sColumns);
			}

			for (var i = 0, iLen = json.aaData.length; i < iLen; i++) {
				if (bReOrder) {
					/* If we need to re-order, then create a new array with the correct order and add it */
					var aData = [];
					for (var j = 0, jLen = oSettings.aoColumns.length; j < jLen; j++) {
						aData.push(json.aaData[i][aiIndex[j]]);
					}
					_fnAddData(oSettings, aData);
				}
				else {
					/* No re-order required, sever got it "right" - just straight add */
					_fnAddData(oSettings, json.aaData[i]);
				}
			}
			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();

			oSettings.bAjaxDataGet = false;
			_fnDraw(oSettings);
			oSettings.bAjaxDataGet = true;
			_fnProcessingDisplay(oSettings, false);
		}


		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - Options (features) HTML
		 */

		/*
		 * Function: _fnAddOptionsHtml
		 * Purpose:  Add the options to the page HTML for the table
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnAddOptionsHtml(oSettings) {
			/*
			 * Create a temporary, empty, div which we can later on replace with what we have generated
			 * we do it this way to rendering the 'options' html offline - speed :-)
			 */
			var nHolding = document.createElement('div');
			oSettings.nTable.parentNode.insertBefore(nHolding, oSettings.nTable);

			/* 
			 * All DataTables are wrapped in a div - this is not currently optional - backwards 
			 * compatability. It can be removed if you don't want it.
			 */
			oSettings.nTableWrapper = document.createElement('div');
			oSettings.nTableWrapper.className = oSettings.oClasses.sWrapper;
			if (oSettings.sTableId !== '') {
				oSettings.nTableWrapper.setAttribute('id', oSettings.sTableId + '_wrapper');
			}

			/* Track where we want to insert the option */
			var nInsertNode = oSettings.nTableWrapper;

			/* Loop over the user set positioning and place the elements as needed */
			var aDom = oSettings.sDom.split('');
			var nTmp, iPushFeature, cOption, nNewNode, cNext, sClass, j;
			for (var i = 0; i < aDom.length; i++) {
				iPushFeature = 0;
				cOption = aDom[i];

				if (cOption == '<') {
					/* New container div */
					nNewNode = document.createElement('div');

					/* Check to see if we should append a class name to the container */
					cNext = aDom[i + 1];
					if (cNext == "'" || cNext == '"') {
						sClass = "";
						j = 2;
						while (aDom[i + j] != cNext) {
							sClass += aDom[i + j];
							j++;
						}

						/* Replace jQuery UI constants */
						if (sClass == "H") {
							sClass = "fg-toolbar ui-widget-header ui-corner-tl ui-corner-tr ui-helper-clearfix";
						}
						else if (sClass == "F") {
							sClass = "fg-toolbar ui-widget-header ui-corner-bl ui-corner-br ui-helper-clearfix";
						}

						nNewNode.className = sClass;
						i += j; /* Move along the position array */
					}

					nInsertNode.appendChild(nNewNode);
					nInsertNode = nNewNode;
				}
				else if (cOption == '>') {
					/* End container div */
					nInsertNode = nInsertNode.parentNode;
				}
				else if (cOption == 'l' && oSettings.oFeatures.bPaginate && oSettings.oFeatures.bLengthChange) {
					/* Length */
					nTmp = _fnFeatureHtmlLength(oSettings);
					iPushFeature = 1;
				}
				else if (cOption == 'f' && oSettings.oFeatures.bFilter) {
					/* Filter */
					nTmp = _fnFeatureHtmlFilter(oSettings);
					iPushFeature = 1;
				}
				else if (cOption == 'r' && oSettings.oFeatures.bProcessing) {
					/* pRocessing */
					nTmp = _fnFeatureHtmlProcessing(oSettings);
					iPushFeature = 1;
				}
				else if (cOption == 't') {
					/* Table */
					nTmp = _fnFeatureHtmlTable(oSettings);
					iPushFeature = 1;
				}
				else if (cOption == 'i' && oSettings.oFeatures.bInfo) {
					/* Info */
					nTmp = _fnFeatureHtmlInfo(oSettings);
					iPushFeature = 1;
				}
				else if (cOption == 'p' && oSettings.oFeatures.bPaginate) {
					/* Pagination */
					nTmp = _fnFeatureHtmlPaginate(oSettings);
					iPushFeature = 1;
				}
				else if (_oExt.aoFeatures.length !== 0) {
					/* Plug-in features */
					var aoFeatures = _oExt.aoFeatures;
					for (var k = 0, kLen = aoFeatures.length; k < kLen; k++) {
						if (cOption == aoFeatures[k].cFeature) {
							nTmp = aoFeatures[k].fnInit(oSettings);
							if (nTmp) {
								iPushFeature = 1;
							}
							break;
						}
					}
				}

				/* Add to the 2D features array */
				if (iPushFeature == 1) {
					if (typeof oSettings.aanFeatures[cOption] != 'object') {
						oSettings.aanFeatures[cOption] = [];
					}
					oSettings.aanFeatures[cOption].push(nTmp);
					nInsertNode.appendChild(nTmp);
				}
			}

			/* Built our DOM structure - replace the holding div with what we want */
			nHolding.parentNode.replaceChild(oSettings.nTableWrapper, nHolding);
		}


		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - Feature: Filtering
		 */

		/*
		 * Function: _fnFeatureHtmlTable
		 * Purpose:  Add any control elements for the table - specifically scrolling
		 * Returns:  node: - Node to add to the DOM
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnFeatureHtmlTable(oSettings) {
			/* Chack if scrolling is enabled or not - if not then leave the DOM unaltered */
			if (oSettings.oScroll.sX === "" && oSettings.oScroll.sY === "") {
				return oSettings.nTable;
			}

			/*
			 * The HTML structure that we want to generate in this function is:
			 *  div - nScroller
			 *    div - nScrollHead
			 *      div - nScrollHeadInner
			 *        table - nScrollHeadTable
			 *          thead - nThead
			 *    div - nScrollBody
			 *      table - oSettings.nTable
			 *        thead - nTheadSize
			 *        tbody - nTbody
			 *    div - nScrollFoot
			 *      div - nScrollFootInner
			 *        table - nScrollFootTable
			 *          tfoot - nTfoot
			 */
			var
				nScroller = document.createElement('div'),
				nScrollHead = document.createElement('div'),
				nScrollHeadInner = document.createElement('div'),
				nScrollBody = document.createElement('div'),
				nScrollFoot = document.createElement('div'),
				nScrollFootInner = document.createElement('div'),
				nScrollHeadTable = oSettings.nTable.cloneNode(false),
				nScrollFootTable = oSettings.nTable.cloneNode(false),
				nThead = oSettings.nTable.getElementsByTagName('thead')[0],
				nTfoot = oSettings.nTable.getElementsByTagName('tfoot').length === 0 ? null :
					oSettings.nTable.getElementsByTagName('tfoot')[0],
				oClasses = (typeof oInit.bJQueryUI != 'undefined' && oInit.bJQueryUI) ?
					_oExt.oJUIClasses : _oExt.oStdClasses;

			nScrollHead.appendChild(nScrollHeadInner);
			nScrollFoot.appendChild(nScrollFootInner);
			nScrollBody.appendChild(oSettings.nTable);
			nScroller.appendChild(nScrollHead);
			nScroller.appendChild(nScrollBody);
			nScrollHeadInner.appendChild(nScrollHeadTable);
			nScrollHeadTable.appendChild(nThead);
			if (nTfoot !== null) {
				nScroller.appendChild(nScrollFoot);
				nScrollFootInner.appendChild(nScrollFootTable);
				nScrollFootTable.appendChild(nTfoot);
			}

			nScroller.className = oClasses.sScrollWrapper;
			nScrollHead.className = oClasses.sScrollHead;
			nScrollHeadInner.className = oClasses.sScrollHeadInner;
			nScrollBody.className = oClasses.sScrollBody;
			nScrollFoot.className = oClasses.sScrollFoot;
			nScrollFootInner.className = oClasses.sScrollFootInner;

			nScrollHead.style.overflow = "hidden";
			nScrollFoot.style.overflow = "hidden";
			nScrollBody.style.overflow = "auto";
			nScrollHead.style.border = "0";
			nScrollFoot.style.border = "0";
			nScrollHeadInner.style.width = "150%"; /* will be overwritten */

			/* Modify attributes to respect the clones */
			nScrollHeadTable.removeAttribute('id');
			nScrollHeadTable.style.marginLeft = "0";
			oSettings.nTable.style.marginLeft = "0";
			if (nTfoot !== null) {
				nScrollFootTable.removeAttribute('id');
				nScrollFootTable.style.marginLeft = "0";
			}

			/* Move any caption elements from the body to the header */
			var nCaptions = $('>caption', oSettings.nTable);
			for (var i = 0, iLen = nCaptions.length; i < iLen; i++) {
				nScrollHeadTable.appendChild(nCaptions[i]);
			}

			/*
			 * Sizing
			 */
			/* When xscrolling add the width and a scroller to move the header with the body */
			if (oSettings.oScroll.sX !== "") {
				nScrollHead.style.width = _fnStringToCss(oSettings.oScroll.sX);
				nScrollBody.style.width = _fnStringToCss(oSettings.oScroll.sX);

				if (nTfoot !== null) {
					nScrollFoot.style.width = _fnStringToCss(oSettings.oScroll.sX);
				}

				/* When the body is scrolled, then we also want to scroll the headers */
				$(nScrollBody).scroll(function (e) {
					nScrollHead.scrollLeft = this.scrollLeft;

					if (nTfoot !== null) {
						nScrollFoot.scrollLeft = this.scrollLeft;
					}
				});
			}

			/* When yscrolling, add the height */
			if (oSettings.oScroll.sY !== "") {
				nScrollBody.style.height = _fnStringToCss(oSettings.oScroll.sY);
			}

			/*
			 * Redraw - align columns across the tables
			 */
			oSettings.aoDrawCallback.push({
				"fn": _fnScrollDraw,
				"sName": "scrolling"
			});

			oSettings.nScrollHead = nScrollHead;
			oSettings.nScrollFoot = nScrollFoot;

			return nScroller;
		}

		/*
		 * Function: _fnScrollDraw
		 * Purpose:  Update the various tables for resizing
		 * Returns:  node: - Node to add to the DOM
		 * Inputs:   object:o - dataTables settings object
		 * Notes:    It's a bit of a pig this function, but basically the idea to:
		 *   1. Re-create the table inside the scrolling div
		 *   2. Take live measurements from the DOM
		 *   3. Apply the measurements
		 *   4. Clean up
		 */
		function _fnScrollDraw(o) {
			var
				nScrollHeadInner = o.nScrollHead.getElementsByTagName('div')[0],
				nScrollHeadTable = nScrollHeadInner.getElementsByTagName('table')[0],
				nScrollBody = o.nTable.parentNode,
				i, iLen, j, jLen, anHeadToSize, anHeadSizers, anFootSizers, anFootToSize, oStyle, iVis,
				iWidth, aApplied = [], iSanityWidth;

			/*
			 * 1. Re-create the table inside the scrolling div
			 */

			/* Remove the old minimised thead and tfoot elements in the inner table */
			var nTheadSize = o.nTable.getElementsByTagName('thead');
			if (nTheadSize.length > 0) {
				o.nTable.removeChild(nTheadSize[0]);
			}

			if (o.nTFoot !== null) {
				/* Remove the old minimised footer element in the cloned header */
				var nTfootSize = o.nTable.getElementsByTagName('tfoot');
				if (nTfootSize.length > 0) {
					o.nTable.removeChild(nTfootSize[0]);
				}
			}

			/* Clone the current header and footer elements and then place it into the inner table */
			nTheadSize = o.nTHead.cloneNode(true);
			o.nTable.insertBefore(nTheadSize, o.nTable.childNodes[0]);

			if (o.nTFoot !== null) {
				nTfootSize = o.nTFoot.cloneNode(true);
				o.nTable.insertBefore(nTfootSize, o.nTable.childNodes[1]);
			}

			/*
			 * 2. Take live measurements from the DOM - do not alter the DOM itself!
			 */

			/* Remove old sizing and apply the calculated column widths
			 * Get the unique column headers in the newly created (cloned) header. We want to apply the
			 * calclated sizes to this header
			 */
			var nThs = _fnGetUniqueThs(nTheadSize);
			for (i = 0, iLen = nThs.length; i < iLen; i++) {
				iVis = _fnVisibleToColumnIndex(o, i);
				nThs[i].style.width = o.aoColumns[iVis].sWidth;
			}

			if (o.nTFoot !== null) {
				_fnApplyToChildren(function (n) {
					n.style.width = "";
				}, nTfootSize.getElementsByTagName('tr'));
			}

			/* Size the table as a whole */
			iSanityWidth = $(o.nTable).outerWidth();
			if (o.oScroll.sX === "") {
				/* No x scrolling */
				o.nTable.style.width = "100%";

				/* I know this is rubbish - but IE7 will make the width of the table when 100% include
				 * the scrollbar - which is shouldn't. This needs feature detection in future - to do
				 */
				if ($.browser.msie && $.browser.version <= 7) {
					o.nTable.style.width = _fnStringToCss($(o.nTable).outerWidth() - o.oScroll.iBarWidth);
				}
			}
			else {
				if (o.oScroll.sXInner !== "") {
					/* x scroll inner has been given - use it */
					o.nTable.style.width = _fnStringToCss(o.oScroll.sXInner);
				}
				else if (iSanityWidth == $(nScrollBody).width() &&
					$(nScrollBody).height() < $(o.nTable).height()) {
					/* There is y-scrolling - try to take account of the y scroll bar */
					o.nTable.style.width = _fnStringToCss(iSanityWidth - o.oScroll.iBarWidth);
					if ($(o.nTable).outerWidth() > iSanityWidth - o.oScroll.iBarWidth) {
						/* Not possible to take account of it */
						o.nTable.style.width = _fnStringToCss(iSanityWidth);
					}
				}
				else {
					/* All else fails */
					o.nTable.style.width = _fnStringToCss(iSanityWidth);
				}
			}

			/* Recalculate the sanity width - now that we've applied the required width, before it was
			 * a temporary variable. This is required because the column width calculation is done
			 * before this table DOM is created.
			 */
			iSanityWidth = $(o.nTable).outerWidth();

			/* We want the hidden header to have zero height, so remove padding and borders. Then
			 * set the width based on the real headers
			 */
			anHeadToSize = o.nTHead.getElementsByTagName('tr');
			anHeadSizers = nTheadSize.getElementsByTagName('tr');

			_fnApplyToChildren(function (nSizer, nToSize) {
				oStyle = nSizer.style;
				oStyle.paddingTop = "0";
				oStyle.paddingBottom = "0";
				oStyle.borderTopWidth = "0";
				oStyle.borderBottomWidth = "0";
				oStyle.height = 0;

				iWidth = $(nSizer).width();
				nToSize.style.width = _fnStringToCss(iWidth);
				aApplied.push(iWidth);
			}, anHeadSizers, anHeadToSize);

			if (o.nTFoot !== null) {
				/* Clone the current footer and then place it into the body table as a "hidden header" */
				anFootSizers = nTfootSize.getElementsByTagName('tr');
				anFootToSize = o.nTFoot.getElementsByTagName('tr');

				_fnApplyToChildren(function (nSizer, nToSize) {
					oStyle = nSizer.style;
					oStyle.paddingTop = "0";
					oStyle.paddingBottom = "0";
					oStyle.borderTopWidth = "0";
					oStyle.borderBottomWidth = "0";

					iWidth = $(nSizer).width();
					nToSize.style.width = _fnStringToCss(iWidth);
					aApplied.push(iWidth);
				}, anFootSizers, anFootToSize);
			}

			/*
			 * 3. Apply the measurements
			 */

			/* "Hide" the header and footer that we used for the sizing. We want to also fix their width
			 * to what they currently are
			 */
			_fnApplyToChildren(function (nSizer) {
				nSizer.innerHTML = "";
				nSizer.style.width = _fnStringToCss(aApplied.shift());
			}, anHeadSizers);

			if (o.nTFoot !== null) {
				_fnApplyToChildren(function (nSizer) {
					nSizer.innerHTML = "";
					nSizer.style.width = _fnStringToCss(aApplied.shift());
				}, anFootSizers);
			}

			/* Sanity check that the table is of a sensible width. If not then we are going to get
			 * misalignment
			 */
			if ($(o.nTable).outerWidth() < iSanityWidth) {
				if (o.oScroll.sX === "") {
					_fnLog(o, 1, "The table cannot fit into the current element which will cause column" +
						" misalignment. It is suggested that you enable x-scrolling or increase the width" +
						" the table has in which to be drawn");
				}
				else if (o.oScroll.sXInner !== "") {
					_fnLog(o, 1, "The table cannot fit into the current element which will cause column" +
						" misalignment. It is suggested that you increase the sScrollXInner property to" +
						" allow it to draw in a larger area, or simply remove that parameter to allow" +
						" automatic calculation");
				}
			}


			/*
			 * 4. Clean up
			 */

			if (o.oScroll.sY === "") {
				/* IE7< puts a vertical scrollbar in place (when it shouldn't be) due to subtracting
				 * the scrollbar height from the visible display, rather than adding it on. We need to
				 * set the height in order to sort this. Don't want to do it in any other browsers.
				 */
				if ($.browser.msie && $.browser.version <= 7) {
					nScrollBody.style.height = _fnStringToCss(o.nTable.offsetHeight + o.oScroll.iBarWidth);
				}
			}

			if (o.oScroll.sY !== "" && o.oScroll.bCollapse) {
				nScrollBody.style.height = _fnStringToCss(o.oScroll.sY);

				var iExtra = (o.oScroll.sX !== "" && o.nTable.offsetWidth > nScrollBody.offsetWidth) ?
					o.oScroll.iBarWidth : 0;
				if (o.nTable.offsetHeight < nScrollBody.offsetHeight) {
					nScrollBody.style.height = _fnStringToCss($(o.nTable).height() + iExtra);
				}
			}

			/* Finally set the width's of the header and footer tables */
			nScrollHeadTable.style.width = _fnStringToCss($(o.nTable).outerWidth());
			nScrollHeadInner.style.width = _fnStringToCss($(o.nTable).outerWidth() + o.oScroll.iBarWidth);

			if (o.nTFoot !== null) {
				var
					nScrollFootInner = o.nScrollFoot.getElementsByTagName('div')[0],
					nScrollFootTable = nScrollFootInner.getElementsByTagName('table')[0];

				nScrollFootInner.style.width = _fnStringToCss(o.nTable.offsetWidth + o.oScroll.iBarWidth);
				nScrollFootTable.style.width = _fnStringToCss(o.nTable.offsetWidth);
			}
		}

		/*
		 * Function: _fnAjustColumnSizing
		 * Purpose:  Ajust the table column widths for new data
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 * Notes:    You would probably want to do a redraw after calling this function!
		 */
		function _fnAjustColumnSizing(oSettings) {
			/* Not interested in doing column width calculation if autowidth is disabled */
			if (oSettings.oFeatures.bAutoWidth === false) {
				return false;
			}

			_fnCalculateColumnWidths(oSettings);
			for (var i = 0, iLen = oSettings.aoColumns.length; i < iLen; i++) {
				oSettings.aoColumns[i].nTh.style.width = oSettings.aoColumns[i].sWidth;
			}
		}


		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - Feature: Filtering
		 */

		/*
		 * Function: _fnFeatureHtmlFilter
		 * Purpose:  Generate the node required for filtering text
		 * Returns:  node
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnFeatureHtmlFilter(oSettings) {
			var nFilter = document.createElement('div');
			if (oSettings.sTableId !== '' && typeof oSettings.aanFeatures.f == "undefined") {
				nFilter.setAttribute('id', oSettings.sTableId + '_filter');
			}
			nFilter.className = oSettings.oClasses.sFilter;
			var sSpace = oSettings.oLanguage.sSearch === "" ? "" : " ";
			nFilter.innerHTML = oSettings.oLanguage.sSearch + sSpace + '<input type="text" />';

			var jqFilter = $("input", nFilter);
			jqFilter.val(oSettings.oPreviousSearch.sSearch.replace('"', '&quot;'));
			jqFilter.keyup(function (e) {
				/* Update all other filter input elements for the new display */
				var n = oSettings.aanFeatures.f;
				for (var i = 0, iLen = n.length; i < iLen; i++) {
					if (n[i] != this.parentNode) {
						$('input', n[i]).val(this.value);
					}
				}

				/* Now do the filter */
				_fnFilterComplete(oSettings, {
					"sSearch": this.value,
					"bRegex": oSettings.oPreviousSearch.bRegex,
					"bSmart": oSettings.oPreviousSearch.bSmart
				});
			});

			jqFilter.keypress(function (e) {
				/* Prevent default */
				if (e.keyCode == 13) {
					return false;
				}
			});

			return nFilter;
		}

		/*
		 * Function: _fnFilterComplete
		 * Purpose:  Filter the table using both the global filter and column based filtering
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 *           object:oSearch: search information
		 *           int:iForce - optional - force a research of the master array (1) or not (undefined or 0)
		 */
		function _fnFilterComplete(oSettings, oInput, iForce) {
			/* Filter on everything */
			_fnFilter(oSettings, oInput.sSearch, iForce, oInput.bRegex, oInput.bSmart);

			/* Now do the individual column filter */
			for (var i = 0; i < oSettings.aoPreSearchCols.length; i++) {
				_fnFilterColumn(oSettings, oSettings.aoPreSearchCols[i].sSearch, i,
					oSettings.aoPreSearchCols[i].bRegex, oSettings.aoPreSearchCols[i].bSmart);
			}

			/* Custom filtering */
			if (_oExt.afnFiltering.length !== 0) {
				_fnFilterCustom(oSettings);
			}

			/* Tell the draw function we have been filtering */
			oSettings.bFiltered = true;

			/* Redraw the table */
			oSettings._iDisplayStart = 0;
			_fnCalculateEnd(oSettings);
			_fnDraw(oSettings);

			/* Rebuild search array 'offline' */
			_fnBuildSearchArray(oSettings, 0);
		}

		/*
		 * Function: _fnFilterCustom
		 * Purpose:  Apply custom filtering functions
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnFilterCustom(oSettings) {
			var afnFilters = _oExt.afnFiltering;
			for (var i = 0, iLen = afnFilters.length; i < iLen; i++) {
				var iCorrector = 0;
				for (var j = 0, jLen = oSettings.aiDisplay.length; j < jLen; j++) {
					var iDisIndex = oSettings.aiDisplay[j - iCorrector];

					/* Check if we should use this row based on the filtering function */
					if (!afnFilters[i](oSettings, oSettings.aoData[iDisIndex]._aData, iDisIndex)) {
						oSettings.aiDisplay.splice(j - iCorrector, 1);
						iCorrector++;
					}
				}
			}
		}

		/*
		 * Function: _fnFilterColumn
		 * Purpose:  Filter the table on a per-column basis
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 *           string:sInput - string to filter on
		 *           int:iColumn - column to filter
		 *           bool:bRegex - treat search string as a regular expression or not
		 *           bool:bSmart - use smart filtering or not
		 */
		function _fnFilterColumn(oSettings, sInput, iColumn, bRegex, bSmart) {
			if (sInput === "") {
				return;
			}

			var iIndexCorrector = 0;
			var rpSearch = _fnFilterCreateSearch(sInput, bRegex, bSmart);

			for (var i = oSettings.aiDisplay.length - 1; i >= 0; i--) {
				var sData = _fnDataToSearch(oSettings.aoData[oSettings.aiDisplay[i]]._aData[iColumn],
					oSettings.aoColumns[iColumn].sType);
				if (!rpSearch.test(sData)) {
					oSettings.aiDisplay.splice(i, 1);
					iIndexCorrector++;
				}
			}
		}

		/*
		 * Function: _fnFilter
		 * Purpose:  Filter the data table based on user input and draw the table
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 *           string:sInput - string to filter on
		 *           int:iForce - optional - force a research of the master array (1) or not (undefined or 0)
		 *           bool:bRegex - treat as a regular expression or not
		 *           bool:bSmart - perform smart filtering or not
		 */
		function _fnFilter(oSettings, sInput, iForce, bRegex, bSmart) {
			var i;
			var rpSearch = _fnFilterCreateSearch(sInput, bRegex, bSmart);

			/* Check if we are forcing or not - optional parameter */
			if (typeof iForce == 'undefined' || iForce === null) {
				iForce = 0;
			}

			/* Need to take account of custom filtering functions - always filter */
			if (_oExt.afnFiltering.length !== 0) {
				iForce = 1;
			}

			/*
			 * If the input is blank - we want the full data set
			 */
			if (sInput.length <= 0) {
				oSettings.aiDisplay.splice(0, oSettings.aiDisplay.length);
				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
			}
			else {
				/*
				 * We are starting a new search or the new search string is smaller 
				 * then the old one (i.e. delete). Search from the master array
			 	 */
				if (oSettings.aiDisplay.length == oSettings.aiDisplayMaster.length ||
					oSettings.oPreviousSearch.sSearch.length > sInput.length || iForce == 1 ||
					sInput.indexOf(oSettings.oPreviousSearch.sSearch) !== 0) {
					/* Nuke the old display array - we are going to rebuild it */
					oSettings.aiDisplay.splice(0, oSettings.aiDisplay.length);

					/* Force a rebuild of the search array */
					_fnBuildSearchArray(oSettings, 1);

					/* Search through all records to populate the search array
					 * The the oSettings.aiDisplayMaster and asDataSearch arrays have 1 to 1 
					 * mapping
					 */
					for (i = 0; i < oSettings.aiDisplayMaster.length; i++) {
						if (rpSearch.test(oSettings.asDataSearch[i])) {
							oSettings.aiDisplay.push(oSettings.aiDisplayMaster[i]);
						}
					}
				}
				else {
			  	/* Using old search array - refine it - do it this way for speed
			  	 * Don't have to search the whole master array again
			 		 */
					var iIndexCorrector = 0;

					/* Search the current results */
					for (i = 0; i < oSettings.asDataSearch.length; i++) {
						if (!rpSearch.test(oSettings.asDataSearch[i])) {
							oSettings.aiDisplay.splice(i - iIndexCorrector, 1);
							iIndexCorrector++;
						}
					}
				}
			}
			oSettings.oPreviousSearch.sSearch = sInput;
			oSettings.oPreviousSearch.bRegex = bRegex;
			oSettings.oPreviousSearch.bSmart = bSmart;
		}

		/*
		 * Function: _fnBuildSearchArray
		 * Purpose:  Create an array which can be quickly search through
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 *           int:iMaster - use the master data array - optional
		 */
		function _fnBuildSearchArray(oSettings, iMaster) {
			/* Clear out the old data */
			oSettings.asDataSearch.splice(0, oSettings.asDataSearch.length);

			var nTmp = document.createElement('div');
			var aArray = (typeof iMaster != 'undefined' && iMaster == 1) ?
				oSettings.aiDisplayMaster : oSettings.aiDisplay;

			for (var i = 0, iLen = aArray.length; i < iLen; i++) {
				oSettings.asDataSearch[i] = '';
				for (var j = 0, jLen = oSettings.aoColumns.length; j < jLen; j++) {
					if (oSettings.aoColumns[j].bSearchable) {
						var sData = oSettings.aoData[aArray[i]]._aData[j];
						oSettings.asDataSearch[i] += _fnDataToSearch(sData, oSettings.aoColumns[j].sType) + '  ';
					}
				}

				/* If it looks like there is an HTML entity in the string, attempt to decode it */
				if (oSettings.asDataSearch[i].indexOf('&') !== -1) {
					nTmp.innerHTML = oSettings.asDataSearch[i];
					oSettings.asDataSearch[i] = nTmp.textContent ? nTmp.textContent : nTmp.innerText;
				}
			}
		}

		/*
		 * Function: _fnFilterCreateSearch
		 * Purpose:  Build a regular expression object suitable for searching a table
		 * Returns:  RegExp: - constructed object
		 * Inputs:   string:sSearch - string to search for
		 *           bool:bRegex - treat as a regular expression or not
		 *           bool:bSmart - perform smart filtering or not
		 */
		function _fnFilterCreateSearch(sSearch, bRegex, bSmart) {
			var asSearch, sRegExpString;

			if (bSmart) {
				/* Generate the regular expression to use. Something along the lines of:
				 * ^(?=.*?\bone\b)(?=.*?\btwo\b)(?=.*?\bthree\b).*$
				 */
				asSearch = bRegex ? sSearch.split(' ') : _fnEscapeRegex(sSearch).split(' ');
				sRegExpString = '^(?=.*?' + asSearch.join(')(?=.*?') + ').*$';
				return new RegExp(sRegExpString, "i");
			}
			else {
				sSearch = bRegex ? sSearch : _fnEscapeRegex(sSearch);
				return new RegExp(sSearch, "i");
			}
		}

		/*
		 * Function: _fnDataToSearch
		 * Purpose:  Convert raw data into something that the user can search on
		 * Returns:  string: - search string
		 * Inputs:   string:sData - data to be modified
		 *           string:sType - data type
		 */
		function _fnDataToSearch(sData, sType) {
			if (typeof _oExt.ofnSearch[sType] == "function") {
				return _oExt.ofnSearch[sType](sData);
			}
			else if (sType == "html") {
				return sData.replace(/\n/g, " ").replace(/<.*?>/g, "");
			}
			else if (typeof sData == "string") {
				return sData.replace(/\n/g, " ");
			}
			return sData;
		}


		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - Feature: Sorting
		 */

		/*
	 	 * Function: _fnSort
		 * Purpose:  Change the order of the table
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 *           bool:bApplyClasses - optional - should we apply classes or not
		 * Notes:    We always sort the master array and then apply a filter again
		 *   if it is needed. This probably isn't optimal - but atm I can't think
		 *   of any other way which is (each has disadvantages). we want to sort aiDisplayMaster - 
		 *   but according to aoData[]._aData
		 */
		function _fnSort(oSettings, bApplyClasses) {
			var aaSort = [];
			var oSort = _oExt.oSort;
			var aoData = oSettings.aoData;
			var iDataSort;
			var iDataType;
			var i, j, jLen;

			/* No sorting required if server-side or no sorting array */
			if (!oSettings.oFeatures.bServerSide &&
				(oSettings.aaSorting.length !== 0 || oSettings.aaSortingFixed !== null)) {
				if (oSettings.aaSortingFixed !== null) {
					aaSort = oSettings.aaSortingFixed.concat(oSettings.aaSorting);
				}
				else {
					aaSort = oSettings.aaSorting.slice();
				}

				/* If there is a sorting data type, and a fuction belonging to it, then we need to
				 * get the data from the developer's function and apply it for this column
				 */
				for (i = 0; i < aaSort.length; i++) {
					var iColumn = aaSort[i][0];
					var iVisColumn = _fnColumnIndexToVisible(oSettings, iColumn);
					var sDataType = oSettings.aoColumns[iColumn].sSortDataType;
					if (typeof _oExt.afnSortData[sDataType] != 'undefined') {
						var aData = _oExt.afnSortData[sDataType](oSettings, iColumn, iVisColumn);
						for (j = 0, jLen = aoData.length; j < jLen; j++) {
							aoData[j]._aData[iColumn] = aData[j];
						}
					}
				}

				/* DataTables offers two different methods for doing the 2D array sorting over multiple
				 * columns. The first is to construct a function dynamically, and then evaluate and run
				 * the function, while the second has no need for evalulation, but is a little bit slower.
				 * This is used for environments which do not allow eval() for code execuation such as AIR
				 */
				if (!window.runtime) {
					/* Dynamically created sorting function. Based on the information that we have, we can
					 * create a sorting function as if it were specifically written for this sort. Here we
					 * want to build a function something like (for two column sorting):
					 *  fnLocalSorting = function(a,b){
					 *  	var iTest;
					 *  	iTest = oSort['string-asc']('data11', 'data12');
					 *  	if (iTest === 0)
					 *  		iTest = oSort['numeric-desc']('data21', 'data22');
					 *  		if (iTest === 0)
					 *  			return oSort['numeric-desc'](1,2);
					 *  	return iTest;
					 *  }
					 * So basically we have a test for each column, and if that column matches, test the
					 * next one. If all columns match, then we use a numeric sort on the position the two
					 * row have in the original data array in order to provide a stable sort.
					 *
					 * Note that for use with the Closure compiler, we need to be very careful how we deal 
					 * with this eval. Closure will rename all of our local variables, resutling in breakage
					 * if the variables in the eval don't also reflect this. For this reason, we need to use
					 * 'this' to store the variables we need in the eval, so we can control them. A little
					 * nasty, but well worth it for using Closure.
					 */
					this.ClosureDataTables = {
						"fn": function () { },
						"data": aoData,
						"sort": _oExt.oSort
					};
					var sDynamicSort = "this.ClosureDataTables.fn = function(a,b){" +
						"var iTest, oSort=this.ClosureDataTables.sort, " +
						"aoData=this.ClosureDataTables.data;";

					for (i = 0; i < aaSort.length - 1; i++) {
						iDataSort = oSettings.aoColumns[aaSort[i][0]].iDataSort;
						iDataType = oSettings.aoColumns[iDataSort].sType;
						sDynamicSort += "iTest = oSort['" + iDataType + "-" + aaSort[i][1] + "']" +
							"( aoData[a]._aData[" + iDataSort + "], aoData[b]._aData[" + iDataSort + "] ); if ( iTest === 0 )";
					}

					if (aaSort.length > 0) {
						iDataSort = oSettings.aoColumns[aaSort[aaSort.length - 1][0]].iDataSort;
						iDataType = oSettings.aoColumns[iDataSort].sType;
						sDynamicSort += "iTest = oSort['" + iDataType + "-" + aaSort[aaSort.length - 1][1] + "']" +
							"( aoData[a]._aData[" + iDataSort + "], aoData[b]._aData[" + iDataSort + "] );" +
							"if (iTest===0) return oSort['numeric-" + aaSort[aaSort.length - 1][1] + "'](a, b); " +
							"return iTest;}";

						/* The eval has to be done to a variable for IE */
						eval(sDynamicSort);
						oSettings.aiDisplayMaster.sort(this.ClosureDataTables.fn);
					}
					this.ClosureDataTables = undefined;
				}
				else {
					/*
					 * Non-eval() sorting (AIR and other environments which doesn't allow code in eval()
					 * Note that for reasonable sized data sets this method is around 1.5 times slower than
					 * the eval above (hence why it is not used all the time). Oddly enough, it is ever so
					 * slightly faster for very small sets (presumably the eval has overhead).
					 *   Single column (1083 records) - eval: 32mS   AIR: 38mS
					 *   Two columns (1083 records) -   eval: 55mS   AIR: 66mS
					 */

					/* Build a cached array so the sort doesn't have to process this stuff on every call */
					var aAirSort = [];
					var iLen = aaSort.length;
					for (i = 0; i < iLen; i++) {
						iDataSort = oSettings.aoColumns[aaSort[i][0]].iDataSort;
						aAirSort.push([
							iDataSort,
							oSettings.aoColumns[iDataSort].sType + '-' + aaSort[i][1]
						]);
					}

					oSettings.aiDisplayMaster.sort(function (a, b) {
						var iTest;
						for (var i = 0; i < iLen; i++) {
							iTest = oSort[aAirSort[i][1]](aoData[a]._aData[aAirSort[i][0]], aoData[b]._aData[aAirSort[i][0]]);
							if (iTest !== 0) {
								return iTest;
							}
						}
						return 0;
					});
				}
			}

			/* Alter the sorting classes to take account of the changes */
			if (typeof bApplyClasses == 'undefined' || bApplyClasses) {
				_fnSortingClasses(oSettings);
			}

			/* Tell the draw function that we have sorted the data */
			oSettings.bSorted = true;

			/* Copy the master data into the draw array and re-draw */
			if (oSettings.oFeatures.bFilter) {
				/* _fnFilter() will redraw the table for us */
				_fnFilterComplete(oSettings, oSettings.oPreviousSearch, 1);
			}
			else {
				oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();
				oSettings._iDisplayStart = 0; /* reset display back to page 0 */
				_fnCalculateEnd(oSettings);
				_fnDraw(oSettings);
			}
		}

		/*
		 * Function: _fnSortAttachListener
		 * Purpose:  Attach a sort handler (click) to a node
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 *           node:nNode - node to attach the handler to
		 *           int:iDataIndex - column sorting index
		 *           function:fnCallback - callback function - optional
		 */
		function _fnSortAttachListener(oSettings, nNode, iDataIndex, fnCallback) {
			$(nNode).click(function (e) {
				/* If the column is not sortable - don't to anything */
				if (oSettings.aoColumns[iDataIndex].bSortable === false) {
					return;
				}

				/*
				 * This is a little bit odd I admit... I declare a temporary function inside the scope of
				 * _fnDrawHead and the click handler in order that the code presented here can be used 
				 * twice - once for when bProcessing is enabled, and another time for when it is 
				 * disabled, as we need to perform slightly different actions.
				 *   Basically the issue here is that the Javascript engine in modern browsers don't 
				 * appear to allow the rendering engine to update the display while it is still excuting
				 * it's thread (well - it does but only after long intervals). This means that the 
				 * 'processing' display doesn't appear for a table sort. To break the js thread up a bit
				 * I force an execution break by using setTimeout - but this breaks the expected 
				 * thread continuation for the end-developer's point of view (their code would execute
				 * too early), so we on;y do it when we absolutely have to.
				 */
				var fnInnerSorting = function () {
					var iColumn, iNextSort;

					/* If the shift key is pressed then we are multipe column sorting */
					if (e.shiftKey) {
						/* Are we already doing some kind of sort on this column? */
						var bFound = false;
						for (var i = 0; i < oSettings.aaSorting.length; i++) {
							if (oSettings.aaSorting[i][0] == iDataIndex) {
								bFound = true;
								iColumn = oSettings.aaSorting[i][0];
								iNextSort = oSettings.aaSorting[i][2] + 1;

								if (typeof oSettings.aoColumns[iColumn].asSorting[iNextSort] == 'undefined') {
									/* Reached the end of the sorting options, remove from multi-col sort */
									oSettings.aaSorting.splice(i, 1);
								}
								else {
									/* Move onto next sorting direction */
									oSettings.aaSorting[i][1] = oSettings.aoColumns[iColumn].asSorting[iNextSort];
									oSettings.aaSorting[i][2] = iNextSort;
								}
								break;
							}
						}

						/* No sort yet - add it in */
						if (bFound === false) {
							oSettings.aaSorting.push([iDataIndex,
								oSettings.aoColumns[iDataIndex].asSorting[0], 0]);
						}
					}
					else {
						/* If no shift key then single column sort */
						if (oSettings.aaSorting.length == 1 && oSettings.aaSorting[0][0] == iDataIndex) {
							iColumn = oSettings.aaSorting[0][0];
							iNextSort = oSettings.aaSorting[0][2] + 1;
							if (typeof oSettings.aoColumns[iColumn].asSorting[iNextSort] == 'undefined') {
								iNextSort = 0;
							}
							oSettings.aaSorting[0][1] = oSettings.aoColumns[iColumn].asSorting[iNextSort];
							oSettings.aaSorting[0][2] = iNextSort;
						}
						else {
							oSettings.aaSorting.splice(0, oSettings.aaSorting.length);
							oSettings.aaSorting.push([iDataIndex,
								oSettings.aoColumns[iDataIndex].asSorting[0], 0]);
						}
					}

					/* Run the sort */
					_fnSort(oSettings);
				}; /* /fnInnerSorting */

				if (!oSettings.oFeatures.bProcessing) {
					fnInnerSorting();
				}
				else {
					_fnProcessingDisplay(oSettings, true);
					setTimeout(function () {
						fnInnerSorting();
						if (!oSettings.oFeatures.bServerSide) {
							_fnProcessingDisplay(oSettings, false);
						}
					}, 0);
				}

				/* Call the user specified callback function - used for async user interaction */
				if (typeof fnCallback == 'function') {
					fnCallback(oSettings);
				}
			});
		}

		/*
		 * Function: _fnSortingClasses
		 * Purpose:  Set the sortting classes on the header
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 * Notes:    It is safe to call this function when bSort is false
		 */
		function _fnSortingClasses(oSettings) {
			var i, iLen, j, jLen, iFound;
			var aaSort, sClass;
			var iColumns = oSettings.aoColumns.length;
			var oClasses = oSettings.oClasses;

			for (i = 0; i < iColumns; i++) {
				if (oSettings.aoColumns[i].bSortable) {
					$(oSettings.aoColumns[i].nTh).removeClass(oClasses.sSortAsc + " " + oClasses.sSortDesc +
						" " + oSettings.aoColumns[i].sSortingClass);
				}
			}

			if (oSettings.aaSortingFixed !== null) {
				aaSort = oSettings.aaSortingFixed.concat(oSettings.aaSorting);
			}
			else {
				aaSort = oSettings.aaSorting.slice();
			}

			/* Apply the required classes to the header */
			for (i = 0; i < oSettings.aoColumns.length; i++) {
				if (oSettings.aoColumns[i].bSortable) {
					sClass = oSettings.aoColumns[i].sSortingClass;
					iFound = -1;
					for (j = 0; j < aaSort.length; j++) {
						if (aaSort[j][0] == i) {
							sClass = (aaSort[j][1] == "asc") ?
								oClasses.sSortAsc : oClasses.sSortDesc;
							iFound = j;
							break;
						}
					}
					$(oSettings.aoColumns[i].nTh).addClass(sClass);

					if (oSettings.bJUI) {
						/* jQuery UI uses extra markup */
						var jqSpan = $("span", oSettings.aoColumns[i].nTh);
						jqSpan.removeClass(oClasses.sSortJUIAsc + " " + oClasses.sSortJUIDesc + " " +
							oClasses.sSortJUI + " " + oClasses.sSortJUIAscAllowed + " " + oClasses.sSortJUIDescAllowed);

						var sSpanClass;
						if (iFound == -1) {
							sSpanClass = oSettings.aoColumns[i].sSortingClassJUI;
						}
						else if (aaSort[iFound][1] == "asc") {
							sSpanClass = oClasses.sSortJUIAsc;
						}
						else {
							sSpanClass = oClasses.sSortJUIDesc;
						}

						jqSpan.addClass(sSpanClass);
					}
				}
				else {
					/* No sorting on this column, so add the base class. This will have been assigned by
					 * _fnAddColumn
					 */
					$(oSettings.aoColumns[i].nTh).addClass(oSettings.aoColumns[i].sSortingClass);
				}
			}

			/* 
			 * Apply the required classes to the table body
			 * Note that this is given as a feature switch since it can significantly slow down a sort
			 * on large data sets (adding and removing of classes is always slow at the best of times..)
			 * Further to this, note that this code is admitadly fairly ugly. It could be made a lot 
			 * simpiler using jQuery selectors and add/removeClass, but that is significantly slower
			 * (on the order of 5 times slower) - hence the direct DOM manipulation here.
			 */
			sClass = oClasses.sSortColumn;

			if (oSettings.oFeatures.bSort && oSettings.oFeatures.bSortClasses) {
				var nTds = _fnGetTdNodes(oSettings);

				/* Remove the old classes */
				if (nTds.length >= iColumns) {
					for (i = 0; i < iColumns; i++) {
						if (nTds[i].className.indexOf(sClass + "1") != -1) {
							for (j = 0, jLen = (nTds.length / iColumns); j < jLen; j++) {
								nTds[(iColumns * j) + i].className =
									nTds[(iColumns * j) + i].className.replace(" " + sClass + "1", "");
							}
						}
						else if (nTds[i].className.indexOf(sClass + "2") != -1) {
							for (j = 0, jLen = (nTds.length / iColumns); j < jLen; j++) {
								nTds[(iColumns * j) + i].className =
									nTds[(iColumns * j) + i].className.replace(" " + sClass + "2", "");
							}
						}
						else if (nTds[i].className.indexOf(sClass + "3") != -1) {
							for (j = 0, jLen = (nTds.length / iColumns); j < jLen; j++) {
								nTds[(iColumns * j) + i].className =
									nTds[(iColumns * j) + i].className.replace(" " + sClass + "3", "");
							}
						}
					}
				}

				/* Add the new classes to the table */
				var iClass = 1, iTargetCol;
				for (i = 0; i < aaSort.length; i++) {
					iTargetCol = parseInt(aaSort[i][0], 10);
					for (j = 0, jLen = (nTds.length / iColumns); j < jLen; j++) {
						nTds[(iColumns * j) + iTargetCol].className += " " + sClass + iClass;
					}

					if (iClass < 3) {
						iClass++;
					}
				}
			}
		}


		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - Feature: Pagination. Note that most of the paging logic is done in 
		 * _oExt.oPagination
		 */

		/*
		 * Function: _fnFeatureHtmlPaginate
		 * Purpose:  Generate the node required for default pagination
		 * Returns:  node
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnFeatureHtmlPaginate(oSettings) {
			var nPaginate = document.createElement('div');
			nPaginate.className = oSettings.oClasses.sPaging + oSettings.sPaginationType;

			_oExt.oPagination[oSettings.sPaginationType].fnInit(oSettings, nPaginate,
				function (oSettings) {
					_fnCalculateEnd(oSettings);
					_fnDraw(oSettings);
				}
			);

			/* Add a draw callback for the pagination on first instance, to update the paging display */
			if (typeof oSettings.aanFeatures.p == "undefined") {
				oSettings.aoDrawCallback.push({
					"fn": function (oSettings) {
						_oExt.oPagination[oSettings.sPaginationType].fnUpdate(oSettings, function (oSettings) {
							_fnCalculateEnd(oSettings);
							_fnDraw(oSettings);
						});
					},
					"sName": "pagination"
				});
			}
			return nPaginate;
		}

		/*
		 * Function: _fnPageChange
		 * Purpose:  Alter the display settings to change the page   更改显示设置以更改页面
		 * Returns:  bool:true - page has changed, false - no change (no effect) eg 'first' on page 1
		 * Inputs:   object:oSettings - dataTables settings object
		 *           string:sAction - paging action to take: "first", "previous", "next" or "last"
		 */
		function _fnPageChange(oSettings, sAction) {
			var iOldStart = oSettings._iDisplayStart;

			if (sAction == "first") {
				oSettings._iDisplayStart = 0;
			} else if (sAction == "previous") {
				oSettings._iDisplayStart = oSettings._iDisplayLength >= 0 ?
					oSettings._iDisplayStart - oSettings._iDisplayLength :
					0;
				/* Correct for underrun 纠正欠载运行*/
				if (oSettings._iDisplayStart < 0) {
					oSettings._iDisplayStart = 0;
				}
			} else if (sAction == "next") {
				if (oSettings._iDisplayLength >= 0) {
					/* Make sure we are not over running the display array */
					if (oSettings._iDisplayStart + oSettings._iDisplayLength < oSettings.fnRecordsDisplay()) {
						oSettings._iDisplayStart += oSettings._iDisplayLength;
					}
				} else {
					oSettings._iDisplayStart = 0;
				}
			} else if (sAction == "last") {
				if (oSettings._iDisplayLength >= 0) {
					var iPages = parseInt((oSettings.fnRecordsDisplay() - 1) / oSettings._iDisplayLength, 10) + 1;
					oSettings._iDisplayStart = (iPages - 1) * oSettings._iDisplayLength;
				} else {
					oSettings._iDisplayStart = 0;
				}
			} else {
				_fnLog(oSettings, 0, "Unknown paging action: " + sAction);
			}

			return iOldStart != oSettings._iDisplayStart;
		}


		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - Feature: HTML info
		 */

		/*
		 * Function: _fnFeatureHtmlInfo
		 * Purpose:  Generate the node required for the info display
		 * Returns:  node
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnFeatureHtmlInfo(oSettings) {
			var nInfo = document.createElement('div');
			nInfo.className = oSettings.oClasses.sInfo;

			/* Actions that are to be taken once only for this feature */
			if (typeof oSettings.aanFeatures.i == "undefined") {
				/* Add draw callback */
				oSettings.aoDrawCallback.push({
					"fn": _fnUpdateInfo,
					"sName": "information"
				});

				/* Add id */
				if (oSettings.sTableId !== '') {
					nInfo.setAttribute('id', oSettings.sTableId + '_info');
				}
			}

			return nInfo;
		}

		/*
		 * Function: _fnUpdateInfo
		 * Purpose:  Update the information elements in the display
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnUpdateInfo(oSettings) {
			/* Show information about the table */
			if (!oSettings.oFeatures.bInfo || oSettings.aanFeatures.i.length === 0) {
				return;
			}

			var jqFirst = $(oSettings.aanFeatures.i[0]);
			var
				sMax = oSettings.fnFormatNumber(oSettings.fnRecordsTotal()),
				sStart = oSettings.fnFormatNumber(oSettings._iDisplayStart + 1),
				sEnd = oSettings.fnFormatNumber(oSettings.fnDisplayEnd()),
				sTotal = oSettings.fnFormatNumber(oSettings.fnRecordsDisplay());

			if (oSettings.fnRecordsDisplay() === 0 &&
				oSettings.fnRecordsDisplay() == oSettings.fnRecordsTotal()) {
				/* Empty record set */
				jqFirst.html(oSettings.oLanguage.sInfoEmpty + oSettings.oLanguage.sInfoPostFix);
			}
			else if (oSettings.fnRecordsDisplay() === 0) {
				/* Rmpty record set after filtering */
				jqFirst.html(oSettings.oLanguage.sInfoEmpty + ' ' +
					oSettings.oLanguage.sInfoFiltered.replace('_MAX_', sMax) +
					oSettings.oLanguage.sInfoPostFix);
			}
			else if (oSettings.fnRecordsDisplay() == oSettings.fnRecordsTotal()) {
				/* Normal record set */
				jqFirst.html(oSettings.oLanguage.sInfo.
					replace('_START_', sStart).
					replace('_END_', sEnd).
					replace('_TOTAL_', sTotal) +
					oSettings.oLanguage.sInfoPostFix);
			}
			else {
				/* Record set after filtering */
				jqFirst.html(oSettings.oLanguage.sInfo.
					replace('_START_', sStart).
					replace('_END_', sEnd).
					replace('_TOTAL_', sTotal) + ' ' +
					oSettings.oLanguage.sInfoFiltered.replace('_MAX_',
						oSettings.fnFormatNumber(oSettings.fnRecordsTotal())) +
					oSettings.oLanguage.sInfoPostFix);
			}

			/* No point in recalculating for the other info elements, just copy the first one in */
			var n = oSettings.aanFeatures.i;
			if (n.length > 1) {
				var sInfo = jqFirst.html();
				for (var i = 1, iLen = n.length; i < iLen; i++) {
					$(n[i]).html(sInfo);
				}
			}
		}


		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - Feature: Length change
		 */

		/*
		 * Function: _fnFeatureHtmlLength
		 * Purpose:  Generate the node required for user display length changing
		 * Returns:  node
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnFeatureHtmlLength(oSettings) {
			/* This can be overruled by not using the _MENU_ var/macro in the language variable */
			var sName = (oSettings.sTableId === "") ? "" : 'name="' + oSettings.sTableId + '_length"';
			var sStdMenu = '<select size="1" ' + sName + '>';
			var i, iLen;

			if (oSettings.aLengthMenu.length == 2 && typeof oSettings.aLengthMenu[0] == 'object' &&
				typeof oSettings.aLengthMenu[1] == 'object') {
				for (i = 0, iLen = oSettings.aLengthMenu[0].length; i < iLen; i++) {
					sStdMenu += '<option value="' + oSettings.aLengthMenu[0][i] + '">' +
						oSettings.aLengthMenu[1][i] + '</option>';
				}
			}
			else {
				for (i = 0, iLen = oSettings.aLengthMenu.length; i < iLen; i++) {
					sStdMenu += '<option value="' + oSettings.aLengthMenu[i] + '">' +
						oSettings.aLengthMenu[i] + '</option>';
				}
			}
			sStdMenu += '</select>';

			var nLength = document.createElement('div');
			if (oSettings.sTableId !== '' && typeof oSettings.aanFeatures.l == "undefined") {
				nLength.setAttribute('id', oSettings.sTableId + '_length');
			}
			nLength.className = oSettings.oClasses.sLength;
			nLength.innerHTML = oSettings.oLanguage.sLengthMenu.replace('_MENU_', sStdMenu);

			/*
			 * Set the length to the current display length - thanks to Andrea Pavlovic for this fix,
			 * and Stefan Skopnik for fixing the fix!
			 */
			$('select option[value="' + oSettings._iDisplayLength + '"]', nLength).attr("selected", true);

			$('select', nLength).change(function (e) {
				var iVal = $(this).val();

				/* Update all other length options for the new display */
				var n = oSettings.aanFeatures.l;
				for (i = 0, iLen = n.length; i < iLen; i++) {
					if (n[i] != this.parentNode) {
						$('select', n[i]).val(iVal);
					}
				}

				/* Redraw the table */
				oSettings._iDisplayLength = parseInt(iVal, 10);
				_fnCalculateEnd(oSettings);

				/* If we have space to show extra rows (backing up from the end point - then do so */
				if (oSettings.fnDisplayEnd() == oSettings.fnRecordsDisplay()) {
					oSettings._iDisplayStart = oSettings.fnDisplayEnd() - oSettings._iDisplayLength;
					if (oSettings._iDisplayStart < 0) {
						oSettings._iDisplayStart = 0;
					}
				}

				if (oSettings._iDisplayLength == -1) {
					oSettings._iDisplayStart = 0;
				}

				_fnDraw(oSettings);
			});

			return nLength;
		}


		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - Feature: Processing incidator
		 */

		/*
		 * Function: _fnFeatureHtmlProcessing
		 * Purpose:  Generate the node required for the processing node
		 * Returns:  node
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnFeatureHtmlProcessing(oSettings) {
			var nProcessing = document.createElement('div');

			if (oSettings.sTableId !== '' && typeof oSettings.aanFeatures.r == "undefined") {
				nProcessing.setAttribute('id', oSettings.sTableId + '_processing');
			}
			nProcessing.innerHTML = oSettings.oLanguage.sProcessing;
			nProcessing.className = oSettings.oClasses.sProcessing;
			oSettings.nTable.parentNode.insertBefore(nProcessing, oSettings.nTable);

			return nProcessing;
		}

		/*
		 * Function: _fnProcessingDisplay
		 * Purpose:  Display or hide the processing indicator
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 *           bool:
		 *   true - show the processing indicator
		 *   false - don't show
		 */
		function _fnProcessingDisplay(oSettings, bShow) {
			if (oSettings.oFeatures.bProcessing) {
				var an = oSettings.aanFeatures.r;
				for (var i = 0, iLen = an.length; i < iLen; i++) {
					an[i].style.visibility = bShow ? "visible" : "hidden";
				}
			}
		}


		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - Support functions
		 */

		/*
		 * Function: _fnVisibleToColumnIndex
		 * Purpose:  Covert the index of a visible column to the index in the data array (take account
		 *   of hidden columns)
		 * Returns:  int:i - the data index
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnVisibleToColumnIndex(oSettings, iMatch) {
			var iColumn = -1;

			for (var i = 0; i < oSettings.aoColumns.length; i++) {
				if (oSettings.aoColumns[i].bVisible === true) {
					iColumn++;
				}

				if (iColumn == iMatch) {
					return i;
				}
			}

			return null;
		}

		/*
		 * Function: _fnColumnIndexToVisible
		 * Purpose:  Covert the index of an index in the data array and convert it to the visible
		 *   column index (take account of hidden columns)
		 * Returns:  int:i - the data index
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnColumnIndexToVisible(oSettings, iMatch) {
			var iVisible = -1;
			for (var i = 0; i < oSettings.aoColumns.length; i++) {
				if (oSettings.aoColumns[i].bVisible === true) {
					iVisible++;
				}

				if (i == iMatch) {
					return oSettings.aoColumns[i].bVisible === true ? iVisible : null;
				}
			}

			return null;
		}


		/*
		 * Function: _fnNodeToDataIndex
		 * Purpose:  Take a TR element and convert it to an index in aoData
		 * Returns:  int:i - index if found, null if not
		 * Inputs:   object:s - dataTables settings object
		 *           node:n - the TR element to find
		 */
		function _fnNodeToDataIndex(s, n) {
			var i, iLen;

			/* Optimisation - see if the nodes which are currently visible match, since that is
			 * the most likely node to be asked for (a selector or event for example)
			 */
			for (i = s._iDisplayStart, iLen = s._iDisplayEnd; i < iLen; i++) {
				if (s.aoData[s.aiDisplay[i]].nTr == n) {
					return s.aiDisplay[i];
				}
			}

			/* Otherwise we are in for a slog through the whole data cache */
			for (i = 0, iLen = s.aoData.length; i < iLen; i++) {
				if (s.aoData[i].nTr == n) {
					return i;
				}
			}
			return null;
		}

		/*
		 * Function: _fnVisbleColumns
		 * Purpose:  Get the number of visible columns
		 * Returns:  int:i - the number of visible columns
		 * Inputs:   object:oS - dataTables settings object
		 */
		function _fnVisbleColumns(oS) {
			var iVis = 0;
			for (var i = 0; i < oS.aoColumns.length; i++) {
				if (oS.aoColumns[i].bVisible === true) {
					iVis++;
				}
			}
			return iVis;
		}

		/*
		 * Function: _fnCalculateEnd
		 * Purpose:  Rcalculate the end point based on the start point
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnCalculateEnd(oSettings) {
			if (oSettings.oFeatures.bPaginate === false) {
				oSettings._iDisplayEnd = oSettings.aiDisplay.length;
			}
			else {
				/* Set the end point of the display - based on how many elements there are
				 * still to display
				 */
				if (oSettings._iDisplayStart + oSettings._iDisplayLength > oSettings.aiDisplay.length ||
					oSettings._iDisplayLength == -1) {
					oSettings._iDisplayEnd = oSettings.aiDisplay.length;
				}
				else {
					oSettings._iDisplayEnd = oSettings._iDisplayStart + oSettings._iDisplayLength;
				}
			}
		}

		/*
		 * Function: _fnConvertToWidth
		 * Purpose:  Convert a CSS unit width to pixels (e.g. 2em)
		 * Returns:  int:iWidth - width in pixels
		 * Inputs:   string:sWidth - width to be converted
		 *           node:nParent - parent to get the with for (required for
		 *             relative widths) - optional
		 */
		function _fnConvertToWidth(sWidth, nParent) {
			if (!sWidth || sWidth === null || sWidth === '') {
				return 0;
			}

			if (typeof nParent == "undefined") {
				nParent = document.getElementsByTagName('body')[0];
			}

			var iWidth;
			var nTmp = document.createElement("div");
			nTmp.style.width = sWidth;

			nParent.appendChild(nTmp);
			iWidth = nTmp.offsetWidth;
			nParent.removeChild(nTmp);

			return (iWidth);
		}

		/*
		 * Function: _fnCalculateColumnWidths
		 * Purpose:  Calculate the width of columns for the table
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnCalculateColumnWidths(oSettings) {
			var iTableWidth = oSettings.nTable.offsetWidth;
			var iUserInputs = 0;
			var iTmpWidth;
			var iVisibleColumns = 0;
			var iColums = oSettings.aoColumns.length;
			var i;
			var oHeaders = $('th', oSettings.nTHead);

			/* Convert any user input sizes into pixel sizes */
			for (i = 0; i < iColums; i++) {
				if (oSettings.aoColumns[i].bVisible) {
					iVisibleColumns++;

					if (oSettings.aoColumns[i].sWidth !== null) {
						iTmpWidth = _fnConvertToWidth(oSettings.aoColumns[i].sWidthOrig,
							oSettings.nTable.parentNode);
						if (iTmpWidth !== null) {
							oSettings.aoColumns[i].sWidth = _fnStringToCss(iTmpWidth);
						}

						iUserInputs++;
					}
				}
			}

			/* If the number of columns in the DOM equals the number that we
			 * have to process in dataTables, then we can use the offsets that are
			 * created by the web-browser. No custom sizes can be set in order for
			 * this to happen
			 */
			if (iColums == oHeaders.length && iUserInputs === 0 && iVisibleColumns == iColums) {
				_fnScrollingWidthAdjust(oSettings, oSettings.nTable);

				for (i = 0; i < oSettings.aoColumns.length; i++) {
					iTmpWidth = $(oHeaders[i]).width();
					if (iTmpWidth !== null) {
						oSettings.aoColumns[i].sWidth = _fnStringToCss(iTmpWidth);
					}
				}
			}
			else {
				/* Otherwise we are going to have to do some calculations to get the width of each column.
				 * Construct a 1 row table with the widest node in the data, and any user defined widths,
				 * then insert it into the DOM and allow the browser to do all the hard work of
				 * calculating table widths.
				 */
				var
					nCalcTmp = oSettings.nTable.cloneNode(false),
					nBody = document.createElement('tbody'),
					nTr = document.createElement('tr'),
					nDivSizing;

				nCalcTmp.removeAttribute("id");
				nCalcTmp.appendChild(oSettings.nTHead.cloneNode(true));
				if (oSettings.nTFoot !== null) {
					nCalcTmp.appendChild(oSettings.nTFoot.cloneNode(true));
					_fnApplyToChildren(function (n) {
						n.style.width = "";
					}, nCalcTmp.getElementsByTagName('tr'));
				}

				nCalcTmp.appendChild(nBody);
				nBody.appendChild(nTr);

				/* Remove any sizing that was previously applied by the styles */
				var jqColSizing = $('thead th', nCalcTmp);
				if (jqColSizing.length === 0) {
					jqColSizing = $('tbody tr:eq(0)>td', nCalcTmp);
				}
				jqColSizing.each(function (i) {
					this.style.width = "";

					var iIndex = _fnVisibleToColumnIndex(oSettings, i);
					if (iIndex !== null && oSettings.aoColumns[iIndex].sWidthOrig !== "") {
						this.style.width = oSettings.aoColumns[iIndex].sWidthOrig;
					}
				});

				/* Find the biggest td for each column and put it into the table */
				for (i = 0; i < iColums; i++) {
					if (oSettings.aoColumns[i].bVisible) {
						var nTd = _fnGetWidestNode(oSettings, i);
						if (nTd !== null) {
							nTd = nTd.cloneNode(true);
							nTr.appendChild(nTd);
						}
					}
				}

				/* Build the table and 'display' it */
				var nWrapper = oSettings.nTable.parentNode;
				nWrapper.appendChild(nCalcTmp);

				if (oSettings.oScroll.sX !== "" && oSettings.oScroll.sXInner !== "") {
					nCalcTmp.style.width = _fnStringToCss(oSettings.oScroll.sXInner);
				}
				else if (oSettings.oScroll.sX !== "") {
					nCalcTmp.style.width = "";
					if ($(nCalcTmp).width() < nWrapper.offsetWidth) {
						nCalcTmp.style.width = _fnStringToCss(nWrapper.offsetWidth);
					}
				}
				else {
					nCalcTmp.style.width = _fnStringToCss(nWrapper.offsetWidth);
				}
				nCalcTmp.style.visibility = "hidden";

				/* Scrolling considerations */
				_fnScrollingWidthAdjust(oSettings, nCalcTmp);

				/* Read the width's calculated by the browser and store them for use by the caller. We
				 * first of all try to use the elements in the body, but it is possible that there are
				 * no elements there, under which circumstances we use the header elements
				 */
				var oNodes = $("tbody tr:eq(0)>td", nCalcTmp);
				if (oNodes.length === 0) {
					oNodes = $("thead tr:eq(0)>th", nCalcTmp);
				}

				var iIndex, iCorrector = 0, iWidth;
				for (i = 0; i < oSettings.aoColumns.length; i++) {
					if (oSettings.aoColumns[i].bVisible) {
						iWidth = $(oNodes[iCorrector]).width();
						if (iWidth !== null && iWidth > 0) {
							oSettings.aoColumns[i].sWidth = _fnStringToCss(iWidth);
						}
						iCorrector++;
					}
				}

				oSettings.nTable.style.width = _fnStringToCss($(nCalcTmp).outerWidth());
				nCalcTmp.parentNode.removeChild(nCalcTmp);
			}
		}

		/*
		 * Function: _fnScrollingWidthAdjust
		 * Purpose:  Adjust a table's width to take account of scrolling
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 *           node:n - table node
		 */
		function _fnScrollingWidthAdjust(oSettings, n) {
			if (oSettings.oScroll.sX === "" && oSettings.oScroll.sY !== "") {
				/* When y-scrolling only, we want to remove the width of the scroll bar so the table
				 * + scroll bar will fit into the area avaialble.
				 */
				var iOrigWidth = $(n).width();
				n.style.width = _fnStringToCss($(n).outerWidth() - oSettings.oScroll.iBarWidth);
			}
			else if (oSettings.oScroll.sX !== "") {
				/* When x-scrolling both ways, fix the table at it's current size, without adjusting */
				n.style.width = _fnStringToCss($(n).outerWidth());
			}
		}

		/*
		 * Function: _fnGetWidestNode
		 * Purpose:  Get the widest node
		 * Returns:  string: - max strlens for each column
		 * Inputs:   object:oSettings - dataTables settings object
		 *           int:iCol - column of interest
		 *           boolean:bFast - Should we use fast (but non-accurate) calculation - optional,
		 *             default true
		 * Notes:    This operation is _expensive_ (!!!). It requires a lot of DOM interaction, but
		 *   this is the only way to reliably get the widest string. For example 'mmm' would be wider
		 *   than 'iiii' so we can't just ocunt characters. If this can be optimised it would be good
		 *   to do so!
		 */
		function _fnGetWidestNode(oSettings, iCol, bFast) {
			/* Use fast not non-accurate calculate based on the strlen */
			if (typeof bFast == 'undefined' || bFast) {
				var iMaxLen = _fnGetMaxLenString(oSettings, iCol);
				var iFastVis = _fnColumnIndexToVisible(oSettings, iCol);
				if (iMaxLen < 0) {
					return null;
				}
				return oSettings.aoData[iMaxLen].nTr.getElementsByTagName('td')[iFastVis];
			}

			/* Use the slow approach, but get high quality answers - note that this code is not actually
			 * used by DataTables by default. If you want to use it you can alter the call to 
			 * _fnGetWidestNode to pass 'false' as the third argument
			 */
			var
				iMax = -1, i, iLen,
				iMaxIndex = -1,
				n = document.createElement('div');

			n.style.visibility = "hidden";
			n.style.position = "absolute";
			document.body.appendChild(n);

			for (i = 0, iLen = oSettings.aoData.length; i < iLen; i++) {
				n.innerHTML = oSettings.aoData[i]._aData[iCol];
				if (n.offsetWidth > iMax) {
					iMax = n.offsetWidth;
					iMaxIndex = i;
				}
			}
			document.body.removeChild(n);

			if (iMaxIndex >= 0) {
				var iVis = _fnColumnIndexToVisible(oSettings, iCol);
				var nRet = oSettings.aoData[iMaxIndex].nTr.getElementsByTagName('td')[iVis];
				if (nRet) {
					return nRet;
				}
			}
			return null;
		}

		/*
		 * Function: _fnGetMaxLenString
		 * Purpose:  Get the maximum strlen for each data column
		 * Returns:  string: - max strlens for each column
		 * Inputs:   object:oSettings - dataTables settings object
		 *           int:iCol - column of interest
		 */
		function _fnGetMaxLenString(oSettings, iCol) {
			var iMax = 0;
			var iMaxIndex = -1;

			for (var i = 0; i < oSettings.aoData.length; i++) {
				var s = oSettings.aoData[i]._aData[iCol];
				if (s.length > iMax) {
					iMax = s.length;
					iMaxIndex = i;
				}
			}

			return iMaxIndex;
		}

		/*
		 * Function: _fnStringToCss
		 * Purpose:  Append a CSS unit (only if required) to a string
		 * Returns:  0 if match, 1 if length is different, 2 if no match
		 * Inputs:   array:aArray1 - first array
		 *           array:aArray2 - second array
		 */
		function _fnStringToCss(s) {
			if (s === null) {
				return "0px";
			}

			if (typeof s == 'number') {
				return s + "px";
			}

			if (s.indexOf('em') != -1 || s.indexOf('%') != -1 || s.indexOf('ex') != -1 ||
				s.indexOf('px') != -1) {
				return s;
			}

			return s + "px";
		}

		/*
		 * Function: _fnArrayCmp
		 * Purpose:  Compare two arrays
		 * Returns:  0 if match, 1 if length is different, 2 if no match
		 * Inputs:   array:aArray1 - first array
		 *           array:aArray2 - second array
		 */
		function _fnArrayCmp(aArray1, aArray2) {
			if (aArray1.length != aArray2.length) {
				return 1;
			}

			for (var i = 0; i < aArray1.length; i++) {
				if (aArray1[i] != aArray2[i]) {
					return 2;
				}
			}

			return 0;
		}

		/*
		 * Function: _fnDetectType
		 * Purpose:  Get the sort type based on an input string
		 * Returns:  string: - type (defaults to 'string' if no type can be detected)
		 * Inputs:   string:sData - data we wish to know the type of
		 * Notes:    This function makes use of the DataTables plugin objct _oExt 
		 *   (.aTypes) such that new types can easily be added.
		 */
		function _fnDetectType(sData) {
			var aTypes = _oExt.aTypes;
			var iLen = aTypes.length;

			for (var i = 0; i < iLen; i++) {
				var sType = aTypes[i](sData);
				if (sType !== null) {
					return sType;
				}
			}

			return 'string';
		}

		/*
		 * Function: _fnSettingsFromNode
		 * Purpose:  Return the settings object for a particular table
		 * Returns:  object: Settings object - or null if not found
		 * Inputs:   node:nTable - table we are using as a dataTable
		 */
		function _fnSettingsFromNode(nTable) {
			for (var i = 0; i < _aoSettings.length; i++) {
				if (_aoSettings[i].nTable == nTable) {
					return _aoSettings[i];
				}
			}

			return null;
		}

		/*
		 * Function: _fnGetDataMaster
		 * Purpose:  Return an array with the full table data
		 * Returns:  array array:aData - Master data array
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnGetDataMaster(oSettings) {
			var aData = [];
			var iLen = oSettings.aoData.length;
			for (var i = 0; i < iLen; i++) {
				aData.push(oSettings.aoData[i]._aData);
			}
			return aData;
		}

		/*
		 * Function: _fnGetTrNodes
		 * Purpose:  Return an array with the TR nodes for the table
		 * Returns:  array: - TR array
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnGetTrNodes(oSettings) {
			var aNodes = [];
			var iLen = oSettings.aoData.length;
			for (var i = 0; i < iLen; i++) {
				aNodes.push(oSettings.aoData[i].nTr);
			}
			return aNodes;
		}

		/*
		 * Function: _fnGetTdNodes
		 * Purpose:  Return an array with the TD nodes for the table
		 * Returns:  array: - TD array
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnGetTdNodes(oSettings) {
			var nTrs = _fnGetTrNodes(oSettings);
			var nTds = [], nTd;
			var anReturn = [];
			var iCorrector;
			var iRow, iRows, iColumn, iColumns;

			for (iRow = 0, iRows = nTrs.length; iRow < iRows; iRow++) {
				nTds = [];
				for (iColumn = 0, iColumns = nTrs[iRow].childNodes.length; iColumn < iColumns; iColumn++) {
					nTd = nTrs[iRow].childNodes[iColumn];
					if (nTd.nodeName.toUpperCase() == "TD") {
						nTds.push(nTd);
					}
				}

				iCorrector = 0;
				for (iColumn = 0, iColumns = oSettings.aoColumns.length; iColumn < iColumns; iColumn++) {
					if (oSettings.aoColumns[iColumn].bVisible) {
						anReturn.push(nTds[iColumn - iCorrector]);
					}
					else {
						anReturn.push(oSettings.aoData[iRow]._anHidden[iColumn]);
						iCorrector++;
					}
				}
			}
			return anReturn;
		}

		/*
		 * Function: _fnEscapeRegex
		 * Purpose:  scape a string stuch that it can be used in a regular expression
		 * Returns:  string: - escaped string
		 * Inputs:   string:sVal - string to escape
		 */
		function _fnEscapeRegex(sVal) {
			var acEscape = ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\', '$', '^'];
			var reReplace = new RegExp('(\\' + acEscape.join('|\\') + ')', 'g');
			return sVal.replace(reReplace, '\\$1');
		}

		/*
		 * Function: _fnDeleteIndex
		 * Purpose:  Take an array of integers (index array) and remove a target integer (value - not 
		 *             the key!)
		 * Returns:  -
		 * Inputs:   a:array int - Index array to target
		 *           int:iTarget - value to find
		 */
		function _fnDeleteIndex(a, iTarget) {
			var iTargetIndex = -1;

			for (var i = 0, iLen = a.length; i < iLen; i++) {
				if (a[i] == iTarget) {
					iTargetIndex = i;
				}
				else if (a[i] > iTarget) {
					a[i]--;
				}
			}

			if (iTargetIndex != -1) {
				a.splice(iTargetIndex, 1);
			}
		}

		/*
		 * Function: _fnReOrderIndex
		 * Purpose:  Figure out how to reorder a display list
		 * Returns:  array int:aiReturn - index list for reordering
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnReOrderIndex(oSettings, sColumns) {
			var aColumns = sColumns.split(',');
			var aiReturn = [];

			for (var i = 0, iLen = oSettings.aoColumns.length; i < iLen; i++) {
				for (var j = 0; j < iLen; j++) {
					if (oSettings.aoColumns[i].sName == aColumns[j]) {
						aiReturn.push(j);
						break;
					}
				}
			}

			return aiReturn;
		}

		/*
		 * Function: _fnColumnOrdering
		 * Purpose:  Get the column ordering that DataTables expects
		 * Returns:  string: - comma separated list of names
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnColumnOrdering(oSettings) {
			var sNames = '';
			for (var i = 0, iLen = oSettings.aoColumns.length; i < iLen; i++) {
				sNames += oSettings.aoColumns[i].sName + ',';
			}
			if (sNames.length == iLen) {
				return "";
			}
			return sNames.slice(0, -1);
		}

		/*
		 * Function: _fnLog
		 * Purpose:  Log an error message
		 * Returns:  -
		 * Inputs:   int:iLevel - log error messages, or display them to the user
		 *           string:sMesg - error message
		 */
		function _fnLog(oSettings, iLevel, sMesg) {
			var sAlert = oSettings.sTableId === "" ?
				"DataTables warning: " + sMesg :
				"DataTables warning (table id = '" + oSettings.sTableId + "'): " + sMesg;

			if (iLevel === 0) {
				if (_oExt.sErrMode == 'alert') {
					alert(sAlert);
				}
				else {
					throw sAlert;
				}
				return;
			}
			else if (typeof console != 'undefined' && typeof console.log != 'undefined') {
				console.log(sAlert);
			}
		}

		/*
		 * Function: _fnClearTable
		 * Purpose:  Nuke the table
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnClearTable(oSettings) {
			oSettings.aoData.length = 0;
			oSettings.aiDisplayMaster.length = 0;
			oSettings.aiDisplay.length = 0;
			_fnCalculateEnd(oSettings);
		}

		/*
		 * Function: _fnSaveState
		 * Purpose:  Save the state of a table in a cookie such that the page can be reloaded
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 */
		function _fnSaveState(oSettings) {
			if (!oSettings.oFeatures.bStateSave) {
				return;
			}

			/* Store the interesting variables */
			var i;
			var sValue = "{";
			sValue += '"iCreate": ' + new Date().getTime() + ',';
			sValue += '"iStart": ' + oSettings._iDisplayStart + ',';
			sValue += '"iEnd": ' + oSettings._iDisplayEnd + ',';
			sValue += '"iLength": ' + oSettings._iDisplayLength + ',';
			sValue += '"sFilter": "' + oSettings.oPreviousSearch.sSearch.replace('"', '\\"') + '",';
			sValue += '"sFilterEsc": ' + !oSettings.oPreviousSearch.bRegex + ',';

			sValue += '"aaSorting": [ ';
			for (i = 0; i < oSettings.aaSorting.length; i++) {
				sValue += "[" + oSettings.aaSorting[i][0] + ",'" + oSettings.aaSorting[i][1] + "'],";
			}
			sValue = sValue.substring(0, sValue.length - 1);
			sValue += "],";

			sValue += '"aaSearchCols": [ ';
			for (i = 0; i < oSettings.aoPreSearchCols.length; i++) {
				sValue += "['" + oSettings.aoPreSearchCols[i].sSearch.replace("'", "\'") +
					"'," + !oSettings.aoPreSearchCols[i].bRegex + "],";
			}
			sValue = sValue.substring(0, sValue.length - 1);
			sValue += "],";

			sValue += '"abVisCols": [ ';
			for (i = 0; i < oSettings.aoColumns.length; i++) {
				sValue += oSettings.aoColumns[i].bVisible + ",";
			}
			sValue = sValue.substring(0, sValue.length - 1);
			sValue += "]";

			sValue += "}";
			_fnCreateCookie(oSettings.sCookiePrefix + oSettings.sInstance, sValue,
				oSettings.iCookieDuration, oSettings.sCookiePrefix);
		}

		/*
		 * Function: _fnLoadState
		 * Purpose:  Attempt to load a saved table state from a cookie // 从cookie 恢复表格状态
		 * Returns:  -
		 * Inputs:   object:oSettings - dataTables settings object
		 *           object:oInit - DataTables init object so we can override settings
		 */
		function _fnLoadState(oSettings, oInit) {
			if (!oSettings.oFeatures.bStateSave) {
				return;
			}

			var oData;
			var sData = _fnReadCookie(oSettings.sCookiePrefix + oSettings.sInstance); // 字符串格式
			if (sData !== null && sData !== '') {
				/* Try/catch the JSON eval - if it is bad then we ignore it */
				try {
					/* Use the JSON library for safety - if it is available */
					if (typeof JSON == 'object' && typeof JSON.parse == 'function') {
						/* DT 1.4.0 used single quotes for a string - JSON.parse doesn't allow this and throws
						 * an error. So for now we can do this. This can be removed in future it is just to
						 * allow the tranfrer to 1.4.1+ to occur
						 */
						oData = JSON.parse(sData.replace(/'/g, '"'));
					}
					else {
						oData = eval('(' + sData + ')');  // 字符串转换为对象
					}
				}
				catch (e) {
					return;
				}



				/* Restore key features */
				oSettings._iDisplayStart = oData.iStart;
				oSettings.iInitDisplayStart = oData.iStart;
				oSettings._iDisplayEnd = oData.iEnd;
				oSettings._iDisplayLength = oData.iLength;
				oSettings.oPreviousSearch.sSearch = oData.sFilter;
				oSettings.aaSorting = oData.aaSorting.slice();
				oSettings.saved_aaSorting = oData.aaSorting.slice();

				/*
				 * Search filtering - global reference added in 1.4.1
				 * Note that we use a 'not' for the value of the regular expression indicator to maintain
				 * compatibility with pre 1.7 versions, where this was basically inverted. Added in 1.7.0
				 */
				if (typeof oData.sFilterEsc != 'undefined') {
					oSettings.oPreviousSearch.bRegex = !oData.sFilterEsc;
				}

				/* Column filtering - added in 1.5.0 beta 6 */
				if (typeof oData.aaSearchCols != 'undefined') {
					for (var i = 0; i < oData.aaSearchCols.length; i++) {
						oSettings.aoPreSearchCols[i] = {
							"sSearch": oData.aaSearchCols[i][0],
							"bRegex": !oData.aaSearchCols[i][1]
						};
					}
				}

				/* Column visibility state - added in 1.5.0 beta 10 */
				if (typeof oData.abVisCols != 'undefined') {
					/* Pass back visibiliy settings to the init handler, but to do not here override
					 * the init object that the user might have passed in
					 */
					oInit.saved_aoColumns = [];
					for (i = 0; i < oData.abVisCols.length; i++) {
						oInit.saved_aoColumns[i] = {};
						oInit.saved_aoColumns[i].bVisible = oData.abVisCols[i];
					}
				}
			}
		}

		/*
		 * Function: _fnCreateCookie
		 * Purpose:  Create a new cookie with a value to store the state of a table
		 * Returns:  -
		 * Inputs:   string:sName - name of the cookie to create
		 *           string:sValue - the value the cookie should take
		 *           int:iSecs - duration of the cookie
		 *           string:sBaseName - sName is made up of the base + file name - this is the base
		 */
		function _fnCreateCookie(sName, sValue, iSecs, sBaseName) {
			var date = new Date();
			date.setTime(date.getTime() + (iSecs * 1000));

			/* 
			 * Shocking but true - it would appear IE has major issues with having the path not having
			 * a trailing slash on it. We need the cookie to be available based on the path, so we
			 * have to append the file name to the cookie name. Appalling. Thanks to vex for adding the
			 * patch to use at least some of the path
			 */
			var aParts = window.location.pathname.split('/');
			var sNameFile = sName + '_' + aParts.pop().replace(/[\/:]/g, "").toLowerCase();
			var sFullCookie = sNameFile + "=" + encodeURIComponent(sValue) +
				"; expires=" + date.toGMTString() +
				"; path=" + aParts.join('/') + "/";

			/* Are we going to go over the cookie limit of 4KiB? If so, try to delete a cookies
			 * belonging to DataTables. This is FAR from bullet proof
			 */
			var sOldName = "", iOldTime = 9999999999999, oData;
			var iLength = _fnReadCookie(sNameFile) !== null ? document.cookie.length :
				sFullCookie.length + document.cookie.length;

			if (iLength + 10 > 4096) /* Magic 10 for padding */ {
				var aCookies = document.cookie.split(';');
				for (var i = 0, iLen = aCookies.length; i < iLen; i++) {
					if (aCookies[i].indexOf(sBaseName) != -1) {
						/* It's a DataTables cookie, so eval it and check the time stamp */
						var aSplitCookie = aCookies[i].split('=');
						try { oData = eval('(' + decodeURIComponent(aSplitCookie[1]) + ')'); }
						catch (e) { continue; }

						if (typeof oData.iCreate != 'undefined' && oData.iCreate < iOldTime) {
							sOldName = aSplitCookie[0];
							iOldTime = oData.iCreate;
						}
					}
				}

				if (sOldName !== "") {
					document.cookie = sOldName + "=; expires=Thu, 01-Jan-1970 00:00:01 GMT; path=" +
						aParts.join('/') + "/";
				}
			}

			document.cookie = sFullCookie;
		}

		/*
		 * Function: _fnReadCookie
		 * Purpose:  Read an old cookie to get a cookie with an old table state
		 * Returns:  string: - contents of the cookie - or null if no cookie with that name found
		 * Inputs:   string:sName - name of the cookie to read
		 */
		function _fnReadCookie(sName) {
			var
				aParts = window.location.pathname.split('/'),
				sNameEQ = sName + '_' + aParts[aParts.length - 1].replace(/[\/:]/g, "").toLowerCase() + '=',
				sCookieContents = document.cookie.split(';');

			for (var i = 0; i < sCookieContents.length; i++) {
				var c = sCookieContents[i];

				while (c.charAt(0) == ' ') {
					c = c.substring(1, c.length);
				}

				if (c.indexOf(sNameEQ) === 0) {
					return decodeURIComponent(c.substring(sNameEQ.length, c.length));
				}
			}
			return null;
		}

		/*
		 * Function: _fnGetUniqueThs
		 * Purpose:  Get an array of unique th elements, one for each column
		 * Returns:  array node:aReturn - list of unique ths
		 * Inputs:   node:nThead - The thead element for the table
		 */
		function _fnGetUniqueThs(nThead) {
			var nTrs = nThead.getElementsByTagName('tr');

			/* Nice simple case */
			if (nTrs.length == 1) {
				return nTrs[0].getElementsByTagName('th');
			}

			/* Otherwise we need to figure out the layout array to get the nodes */
			var aLayout = [], aReturn = [];
			var ROWSPAN = 2, COLSPAN = 3, TDELEM = 4;
			var i, j, k, iLen, jLen, iColumnShifted;
			var fnShiftCol = function (a, i, j) {
				while (typeof a[i][j] != 'undefined') {
					j++;
				}
				return j;
			};
			var fnAddRow = function (i) {
				if (typeof aLayout[i] == 'undefined') {
					aLayout[i] = [];
				}
			};

			/* Calculate a layout array */
			for (i = 0, iLen = nTrs.length; i < iLen; i++) {
				fnAddRow(i);
				var iColumn = 0;
				var nTds = [];

				for (j = 0, jLen = nTrs[i].childNodes.length; j < jLen; j++) {
					if (nTrs[i].childNodes[j].nodeName.toUpperCase() == "TD" ||
						nTrs[i].childNodes[j].nodeName.toUpperCase() == "TH") {
						nTds.push(nTrs[i].childNodes[j]);
					}
				}

				for (j = 0, jLen = nTds.length; j < jLen; j++) {
					var iColspan = nTds[j].getAttribute('colspan') * 1;
					var iRowspan = nTds[j].getAttribute('rowspan') * 1;

					if (!iColspan || iColspan === 0 || iColspan === 1) {
						iColumnShifted = fnShiftCol(aLayout, i, iColumn);
						aLayout[i][iColumnShifted] = (nTds[j].nodeName.toUpperCase() == "TD") ? TDELEM : nTds[j];
						if (iRowspan || iRowspan === 0 || iRowspan === 1) {
							for (k = 1; k < iRowspan; k++) {
								fnAddRow(i + k);
								aLayout[i + k][iColumnShifted] = ROWSPAN;
							}
						}
						iColumn++;
					}
					else {
						iColumnShifted = fnShiftCol(aLayout, i, iColumn);
						for (k = 0; k < iColspan; k++) {
							aLayout[i][iColumnShifted + k] = COLSPAN;
						}
						iColumn += iColspan;
					}
				}
			}

			/* Convert the layout array into a node array
			 * Note the use of aLayout[0] in the outloop, we want the outer loop to occur the same
			 * number of times as there are columns. Unusual having nested loops this way around
			 * but is what we need here.
			 */
			for (i = 0, iLen = aLayout[0].length; i < iLen; i++) {
				for (j = 0, jLen = aLayout.length; j < jLen; j++) {
					if (typeof aLayout[j][i] == 'object') {
						aReturn.push(aLayout[j][i]);
					}
				}
			}

			return aReturn;
		}

		/*
		 * Function: _fnScrollBarWidth
		 * Purpose:  Get the width of a scroll bar in this browser being used
		 * 获取正在使用的浏览器中滚动条的宽度
		 * 
		 * Returns:  int: - width in pixels
		 * Inputs:   -
		 * Notes:    All credit for this function belongs to Alexandre Gomes. Thanks for sharing!
		 * 所有功劳都属于亚历山大戈麦斯。谢谢分享！
		 *   http://www.alexandre-gomes.com/?p=115
		 */ 
		function _fnScrollBarWidth() {
			var inner = document.createElement('p');
			var style = inner.style;
			style.width = "100%";
			style.height = "200px";

			var outer = document.createElement('div');
			style = outer.style;
			style.position = "absolute";
			style.top = "0px";
			style.left = "0px";
			style.visibility = "hidden";
			style.width = "200px";
			style.height = "150px";
			style.overflow = "hidden";
			outer.appendChild(inner);

			document.body.appendChild(outer);
			var w1 = inner.offsetWidth;
			outer.style.overflow = 'scroll';
			var w2 = inner.offsetWidth;
			if (w1 == w2) {
				w2 = outer.clientWidth;
			}

			document.body.removeChild(outer);
			return (w1 - w2);
		}

		/*
		 * Function: _fnApplyToChildren
		 * Purpose:  Apply a given function to the display child nodes of an element array (typically
		 *   TD children of TR rows
		 * Returns:  - (done by reference)
		 * Inputs:   function:fn - Method to apply to the objects
		 *           array nodes:an1 - List of elements to look through for display children
		 *           array nodes:an2 - Another list (identical structure to the first) - optional
		 */
		function _fnApplyToChildren(fn, an1, an2) {
			for (var i = 0, iLen = an1.length; i < iLen; i++) {
				for (var j = 0, jLen = an1[i].childNodes.length; j < jLen; j++) {
					if (an1[i].childNodes[j].nodeType == 1) {
						if (typeof an2 != 'undefined') {
							fn(an1[i].childNodes[j], an2[i].childNodes[j]);
						}
						else {
							fn(an1[i].childNodes[j]);
						}
					}
				}
			}
		}

		/*
		 * Function: _fnMap
		 * Purpose:  See if a property is defined on one object, if so assign it to the other object
		 * Returns:  - (done by reference)
		 * Inputs:   object:oRet - target object
		 *           object:oSrc - source object
		 *           string:sName - property
		 *           string:sMappedName - name to map too - optional, sName used if not given
		 */
		function _fnMap(oRet, oSrc, sName, sMappedName) {
			if (typeof sMappedName == 'undefined') {
				sMappedName = sName;
			}
			if (typeof oSrc[sName] != 'undefined') {
				oRet[sMappedName] = oSrc[sName];
			}
		}

		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - API
		 * 
		 * I'm not overly happy with this solution - I'd much rather that there was a way of getting
		 * a list of all the private functions and do what we need to dynamically - but that doesn't
		 * appear to be possible. Bonkers. A better solution would be to provide a 'bind' type object
		 * To do - bind type method in DTs 2.x.
		 * 我对这个解决方案并不太满意——我更希望有一种方法可以获取所有私有函数的列表，并动态地执行我们需要的操作——但这似乎是不可能的。
		 * 笨蛋。
		 * 更好的解决方案是在dts 2.x中提供一个“bind”类型的object to do-bind类型方法。
		 * 
		 */
		this.oApi._fnExternApiFunc = _fnExternApiFunc;
		this.oApi._fnInitalise = _fnInitalise;
		this.oApi._fnLanguageProcess = _fnLanguageProcess;
		this.oApi._fnAddColumn = _fnAddColumn;
		this.oApi._fnColumnOptions = _fnColumnOptions;
		this.oApi._fnAddData = _fnAddData;
		this.oApi._fnGatherData = _fnGatherData;
		this.oApi._fnDrawHead = _fnDrawHead;
		this.oApi._fnDraw = _fnDraw;
		this.oApi._fnReDraw = _fnReDraw;
		this.oApi._fnAjaxUpdate = _fnAjaxUpdate;
		this.oApi._fnAjaxUpdateDraw = _fnAjaxUpdateDraw;
		this.oApi._fnAddOptionsHtml = _fnAddOptionsHtml;
		this.oApi._fnFeatureHtmlTable = _fnFeatureHtmlTable;
		this.oApi._fnScrollDraw = _fnScrollDraw;
		this.oApi._fnAjustColumnSizing = _fnAjustColumnSizing;
		this.oApi._fnFeatureHtmlFilter = _fnFeatureHtmlFilter;
		this.oApi._fnFilterComplete = _fnFilterComplete;
		this.oApi._fnFilterCustom = _fnFilterCustom;
		this.oApi._fnFilterColumn = _fnFilterColumn;
		this.oApi._fnFilter = _fnFilter;
		this.oApi._fnBuildSearchArray = _fnBuildSearchArray;
		this.oApi._fnFilterCreateSearch = _fnFilterCreateSearch;
		this.oApi._fnDataToSearch = _fnDataToSearch;
		this.oApi._fnSort = _fnSort;
		this.oApi._fnSortAttachListener = _fnSortAttachListener;
		this.oApi._fnSortingClasses = _fnSortingClasses;
		this.oApi._fnFeatureHtmlPaginate = _fnFeatureHtmlPaginate;
		this.oApi._fnPageChange = _fnPageChange;
		this.oApi._fnFeatureHtmlInfo = _fnFeatureHtmlInfo;
		this.oApi._fnUpdateInfo = _fnUpdateInfo;
		this.oApi._fnFeatureHtmlLength = _fnFeatureHtmlLength;
		this.oApi._fnFeatureHtmlProcessing = _fnFeatureHtmlProcessing;
		this.oApi._fnProcessingDisplay = _fnProcessingDisplay;
		this.oApi._fnVisibleToColumnIndex = _fnVisibleToColumnIndex;
		this.oApi._fnColumnIndexToVisible = _fnColumnIndexToVisible;
		this.oApi._fnNodeToDataIndex = _fnNodeToDataIndex;
		this.oApi._fnVisbleColumns = _fnVisbleColumns;
		this.oApi._fnCalculateEnd = _fnCalculateEnd;
		this.oApi._fnConvertToWidth = _fnConvertToWidth;
		this.oApi._fnCalculateColumnWidths = _fnCalculateColumnWidths;
		this.oApi._fnScrollingWidthAdjust = _fnScrollingWidthAdjust;
		this.oApi._fnGetWidestNode = _fnGetWidestNode;
		this.oApi._fnGetMaxLenString = _fnGetMaxLenString;
		this.oApi._fnStringToCss = _fnStringToCss;
		this.oApi._fnArrayCmp = _fnArrayCmp;
		this.oApi._fnDetectType = _fnDetectType;
		this.oApi._fnSettingsFromNode = _fnSettingsFromNode;
		this.oApi._fnGetDataMaster = _fnGetDataMaster;
		this.oApi._fnGetTrNodes = _fnGetTrNodes;
		this.oApi._fnGetTdNodes = _fnGetTdNodes;
		this.oApi._fnEscapeRegex = _fnEscapeRegex;
		this.oApi._fnDeleteIndex = _fnDeleteIndex;
		this.oApi._fnReOrderIndex = _fnReOrderIndex;
		this.oApi._fnColumnOrdering = _fnColumnOrdering;
		this.oApi._fnLog = _fnLog;
		this.oApi._fnClearTable = _fnClearTable;
		this.oApi._fnSaveState = _fnSaveState;
		this.oApi._fnLoadState = _fnLoadState;
		this.oApi._fnCreateCookie = _fnCreateCookie;
		this.oApi._fnReadCookie = _fnReadCookie;
		this.oApi._fnGetUniqueThs = _fnGetUniqueThs;
		this.oApi._fnScrollBarWidth = _fnScrollBarWidth;
		this.oApi._fnApplyToChildren = _fnApplyToChildren;
		this.oApi._fnMap = _fnMap;


		/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
		 * Section - Constructor
		 */

		/* Want to be able to reference "this" inside the this.each function */
		var _that = this;
		return this.each(function () {
			var i = 0, iLen, j, jLen, k, kLen;

			/* Check to see if we are re-initalising a table */
			for (i = 0, iLen = _aoSettings.length; i < iLen; i++) {
				/* Base check on table node */
				if (_aoSettings[i].nTable == this) {
					if (typeof oInit == 'undefined' || (typeof oInit.bRetrieve != 'undefined' && oInit.bRetrieve === true)) {
						return _aoSettings[i].oInstance;
					}
					else if (typeof oInit.bDestroy != 'undefined' && oInit.bDestroy === true) {
						_aoSettings[i].oInstance.fnDestroy();
						break;
					}
					else {
						_fnLog(_aoSettings[i], 0, "Cannot reinitialise DataTable.\n\n" +
							"To retrieve the DataTables object for this table, please pass either no arguments " +
							"to the dataTable() function, or set bRetrieve to true. Alternatively, to destory " +
							"the old table and create a new one, set bDestroy to true (note that a lot of " +
							"changes to the configuration can be made through the API which is usually much " +
							"faster).");
						return;
					}
				}

				/* If the element we are initialising has the same ID as a table which was previously
				 * initialised, but the table nodes don't match (from before) then we destory the old
				 * instance by simply deleting it. This is under the assumption that the table has been
				 * destroyed by other methods. Anyone using non-id selectors will need to do this manually
				 * 如果我们正在初始化的元素与先前初始化的表具有相同的ID，但表节点不匹配（以前），那么我们只需删除它就可以销毁旧实例。
				 * 这是在假设表已被其他方法销毁的情况下进行的。任何使用非ID选择器的人都需要手动执行此操作
				 */
				if (_aoSettings[i].sTableId !== "" && _aoSettings[i].sTableId == this.getAttribute('id')) {
					_aoSettings.splice(i, 1);
					break;
				}
			}

			/* Make a complete and independent copy of the settings object */
			var oSettings = new classSettings();
			_aoSettings.push(oSettings);

			var bInitHandedOff = false;    // ?
			var bUsePassedData = false;     // ?

			/* Set the id */
			var sId = this.getAttribute('id');
			if (sId !== null) {   // id 不为空
				oSettings.sTableId = sId;
				oSettings.sInstance = sId;
			} else {
				oSettings.sInstance = _oExt._oExternConfig.iNextUnique++;
			}

			/* Store 'this' in the settings object for later retrieval */
			oSettings.oInstance = _that;  // _that中有可能匹配了多个表格实例

			/* Set the table node */
			oSettings.nTable = this;

			/* Bind the API functions to the settings, so we can perform actions whenever oSettings is
			 * available
			 */
			oSettings.oApi = _that.oApi;

			/* Store the features that we have available */
			if (typeof oInit != 'undefined' && oInit !== null) {
				oSettings.oInit = oInit;
				_fnMap(oSettings.oFeatures, oInit, "bPaginate");
				_fnMap(oSettings.oFeatures, oInit, "bLengthChange");
				_fnMap(oSettings.oFeatures, oInit, "bFilter");
				_fnMap(oSettings.oFeatures, oInit, "bSort");
				_fnMap(oSettings.oFeatures, oInit, "bInfo");
				_fnMap(oSettings.oFeatures, oInit, "bProcessing");
				_fnMap(oSettings.oFeatures, oInit, "bAutoWidth");
				_fnMap(oSettings.oFeatures, oInit, "bSortClasses");
				_fnMap(oSettings.oFeatures, oInit, "bServerSide");
				_fnMap(oSettings.oScroll, oInit, "sScrollX", "sX");
				_fnMap(oSettings.oScroll, oInit, "sScrollXInner", "sXInner");
				_fnMap(oSettings.oScroll, oInit, "sScrollY", "sY");
				_fnMap(oSettings.oScroll, oInit, "bScrollCollapse", "bCollapse");
				_fnMap(oSettings, oInit, "asStripClasses");
				_fnMap(oSettings, oInit, "fnRowCallback");
				_fnMap(oSettings, oInit, "fnHeaderCallback");
				_fnMap(oSettings, oInit, "fnFooterCallback");
				_fnMap(oSettings, oInit, "fnInitComplete");
				_fnMap(oSettings, oInit, "fnServerData");
				_fnMap(oSettings, oInit, "fnFormatNumber");
				_fnMap(oSettings, oInit, "aaSorting");
				_fnMap(oSettings, oInit, "aaSortingFixed");
				_fnMap(oSettings, oInit, "aLengthMenu");
				_fnMap(oSettings, oInit, "sPaginationType");
				_fnMap(oSettings, oInit, "sAjaxSource");
				_fnMap(oSettings, oInit, "iCookieDuration");
				_fnMap(oSettings, oInit, "sCookiePrefix");
				_fnMap(oSettings, oInit, "sDom");
				_fnMap(oSettings, oInit, "oSearch", "oPreviousSearch");
				_fnMap(oSettings, oInit, "aoSearchCols", "aoPreSearchCols");
				_fnMap(oSettings, oInit, "iDisplayLength", "_iDisplayLength");
				_fnMap(oSettings, oInit, "bJQueryUI", "bJUI");

				if (typeof oInit.fnDrawCallback == 'function') {
					/* Add user given callback function to array */
					oSettings.aoDrawCallback.push({
						"fn": oInit.fnDrawCallback,
						"sName": "user"
					});
				}

				if (oSettings.oFeatures.bServerSide && oSettings.oFeatures.bSort &&
					oSettings.oFeatures.bSortClasses) {
					/* Enable sort classes for server-side processing. Safe to do it here, since server-side
					 * processing must be enabled by the developer
					 */
					oSettings.aoDrawCallback.push({
						"fn": _fnSortingClasses,
						"sName": "server_side_sort_classes"
					});
				}

				if (typeof oInit.bJQueryUI != 'undefined' && oInit.bJQueryUI) { // 使用 jqueryUI 样式
					/* Use the JUI classes object for display. You could clone the oStdClasses object if 
					 * you want to have multiple tables with multiple independent classes 
					 */
					oSettings.oClasses = _oExt.oJUIClasses;

					if (typeof oInit.sDom == 'undefined') {
						/* Set the DOM to use a layout suitable for jQuery UI's theming */
						oSettings.sDom = '<"H"lfr>t<"F"ip>';
					}
				}

				/* Calculate the scroll bar width and cache it for use later on */
				if (oSettings.oScroll.sX !== "" || oSettings.oScroll.sY !== "") {
					oSettings.oScroll.iBarWidth = _fnScrollBarWidth();
				}

				if (typeof oInit.iDisplayStart != 'undefined' && typeof oSettings.iInitDisplayStart == 'undefined') {
					/* Display start point, taking into account the save saving */
					oSettings.iInitDisplayStart = oInit.iDisplayStart;
					oSettings._iDisplayStart = oInit.iDisplayStart;
				}

				/* Must be done after everything which can be overridden by a cookie! */
				if (typeof oInit.bStateSave != 'undefined') {
					oSettings.oFeatures.bStateSave = oInit.bStateSave;
					_fnLoadState(oSettings, oInit);
					oSettings.aoDrawCallback.push({
						"fn": _fnSaveState,
						"sName": "state_save"
					});
				}

				if (typeof oInit.aaData != 'undefined') {
					bUsePassedData = true;
				}

				/* Backwards compatability */
				/* aoColumns / aoData - remove at some point... */
				if (typeof oInit != 'undefined' && typeof oInit.aoData != 'undefined') {
					oInit.aoColumns = oInit.aoData;
				}

				/* Language definitions */
				if (typeof oInit.oLanguage != 'undefined') {
					if (typeof oInit.oLanguage.sUrl != 'undefined' && oInit.oLanguage.sUrl !== "") {
						/* Get the language definitions from a file */
						oSettings.oLanguage.sUrl = oInit.oLanguage.sUrl;
						$.getJSON(oSettings.oLanguage.sUrl, null, function (json) {
							_fnLanguageProcess(oSettings, json, true);
						});
						bInitHandedOff = true;
					} else {
						_fnLanguageProcess(oSettings, oInit.oLanguage, false);
					}
				}
				/* Warning: The _fnLanguageProcess function is async to the remainder of this function due
				 * to the XHR. We use _bInitialised in _fnLanguageProcess() to check this the processing 
				 * below is complete. The reason for spliting it like this is optimisation - we can fire
				 * off the XHR (if needed) and then continue processing the data.
				 */
			}
			else {
				/* Create a dummy object for quick manipulation later on. */
				oInit = {};
			}

			/*
			 * Stripes
			 * Add the strip classes now that we know which classes to apply - unless overruled
			 */
			if (typeof oInit.asStripClasses == 'undefined') {
				oSettings.asStripClasses.push(oSettings.oClasses.sStripOdd);
				oSettings.asStripClasses.push(oSettings.oClasses.sStripEven);
			}

			/* Remove row stripe classes if they are already on the table row  如果行条纹类已经在表行中，则删除它们*/
			var bStripeRemove = false;
			var anRows = $('tbody>tr', this);
			for (i = 0, iLen = oSettings.asStripClasses.length; i < iLen; i++) {
				if (anRows.filter(":lt(2)").hasClass(oSettings.asStripClasses[i])) {
					bStripeRemove = true;
					break;
				}
			}

			if (bStripeRemove) {
				/* Store the classes which we are about to remove so they can be readded on destory */
				oSettings.asDestoryStrips = ['', ''];
				if ($(anRows[0]).hasClass(oSettings.oClasses.sStripOdd)) {
					oSettings.asDestoryStrips[0] += oSettings.oClasses.sStripOdd + " ";
				}
				if ($(anRows[0]).hasClass(oSettings.oClasses.sStripEven)) {
					oSettings.asDestoryStrips[0] += oSettings.oClasses.sStripEven;
				}
				if ($(anRows[1]).hasClass(oSettings.oClasses.sStripOdd)) {
					oSettings.asDestoryStrips[1] += oSettings.oClasses.sStripOdd + " ";
				}
				if ($(anRows[1]).hasClass(oSettings.oClasses.sStripEven)) {
					oSettings.asDestoryStrips[1] += oSettings.oClasses.sStripEven;
				}

				anRows.removeClass(oSettings.asStripClasses.join(' '));
			}

			/*
			 * Columns
			 * See if we should load columns automatically or use defined ones - a bit messy this...
			 */
			var nThead = this.getElementsByTagName('thead');
			var anThs = nThead.length === 0 ? [] : _fnGetUniqueThs(nThead[0]);
			var bUseCols = typeof oInit.aoColumns != 'undefined';

			for (i = 0, iLen = bUseCols ? oInit.aoColumns.length : anThs.length; i < iLen; i++) {
				var oCol = bUseCols ? oInit.aoColumns[i] : null;
				var nTh = anThs ? anThs[i] : null;

				/* Check if we have column visibilty state to restore, and also that the length of the 
				 * state saved columns matches the currently know number of columns
				 */
				if (typeof oInit.saved_aoColumns != 'undefined' && oInit.saved_aoColumns.length == iLen) {
					if (oCol === null) {
						oCol = {};
					}
					oCol.bVisible = oInit.saved_aoColumns[i].bVisible;
				}

				_fnAddColumn(oSettings, nTh);
			}

			/* Add options from column definations */
			if (typeof oInit.aoColumnDefs != 'undefined') {
				/* Loop over the column defs array - loop in reverse so first instace has priority */
				for (i = oInit.aoColumnDefs.length - 1; i >= 0; i--) {
					/* Each column def can target multiple columns, as it is an array */
					var aTargets = oInit.aoColumnDefs[i].aTargets;
					for (j = 0, jLen = aTargets.length; j < jLen; j++) {
						if (typeof aTargets[j] == 'number' && aTargets[j] >= 0) {
							/* 0+ integer, left to right column counting. We add columns which are unknown
							 * automatically. Is this the right behaviour for this? We should at least
							 * log it in future. We cannot do this for the negative or class targets, only here.
							 */
							while (oSettings.aoColumns.length <= aTargets[j]) {
								_fnAddColumn(oSettings);
							}
							_fnColumnOptions(oSettings, aTargets[j], oInit.aoColumnDefs[i]);
						}
						else if (typeof aTargets[j] == 'number' && aTargets[j] < 0) {
							/* Negative integer, right to left column counting */
							_fnColumnOptions(oSettings, oSettings.aoColumns.length + aTargets[j],
								oInit.aoColumnDefs[i]);
						}
						else if (typeof aTargets[j] == 'string') {
							/* Class name matching on TH element */
							for (k = 0, kLen = oSettings.aoColumns.length; k < kLen; k++) {
								if (aTargets[j] == "_all" ||
									oSettings.aoColumns[k].nTh.className.indexOf(aTargets[j]) != -1) {
									_fnColumnOptions(oSettings, k, oInit.aoColumnDefs[i]);
								}
							}
						}
					}
				}
			}

			/* Add options from column array - after the defs array so this has priority */
			if (typeof oInit.aoColumns != 'undefined') {
				for (i = 0, iLen = oInit.aoColumns.length; i < iLen; i++) {
					_fnColumnOptions(oSettings, i, oInit.aoColumns[i]);
				}
			}

			/*
			 * Sorting
			 * Check the aaSorting array
			 */
			for (i = 0, iLen = oSettings.aaSorting.length; i < iLen; i++) {
				var oColumn = oSettings.aoColumns[oSettings.aaSorting[i][0]];

				/* Add a default sorting index */
				if (typeof oSettings.aaSorting[i][2] == 'undefined') {
					oSettings.aaSorting[i][2] = 0;
				}

				/* If aaSorting is not defined, then we use the first indicator in asSorting */
				if (typeof oInit.aaSorting == "undefined" &&
					typeof oSettings.saved_aaSorting == "undefined") {
					oSettings.aaSorting[i][1] = oColumn.asSorting[0];
				}

				/* Set the current sorting index based on aoColumns.asSorting */
				for (j = 0, jLen = oColumn.asSorting.length; j < jLen; j++) {
					if (oSettings.aaSorting[i][1] == oColumn.asSorting[j]) {
						oSettings.aaSorting[i][2] = j;
						break;
					}
				}
			}

			/*
			 * Final init
			 * Sanity check that there is a thead and tfoot. If not let's just create them
			 */
			if (this.getElementsByTagName('thead').length === 0) {
				this.appendChild(document.createElement('thead'));
			}

			if (this.getElementsByTagName('tbody').length === 0) {
				this.appendChild(document.createElement('tbody'));
			}

			oSettings.nTHead = this.getElementsByTagName('thead')[0];
			oSettings.nTBody = this.getElementsByTagName('tbody')[0];
			if (this.getElementsByTagName('tfoot').length > 0) {
				oSettings.nTFoot = this.getElementsByTagName('tfoot')[0];
			}

			/* Check if there is data passing into the constructor */
			if (bUsePassedData) {
				for (i = 0; i < oInit.aaData.length; i++) {
					_fnAddData(oSettings, oInit.aaData[i]);
				}
			}
			else {
				/* Grab the data from the page */
				_fnGatherData(oSettings);
			}

			/* Copy the data index array */
			oSettings.aiDisplay = oSettings.aiDisplayMaster.slice();

			/* Calculate sizes for columns */
			if (oSettings.oFeatures.bAutoWidth) {
				_fnCalculateColumnWidths(oSettings);
			}

			/* Initialisation complete - table can be drawn */
			oSettings.bInitialised = true;

			/* Check if we need to initialise the table (it might not have been handed off to the
			 * language processor)
			 */
			if (bInitHandedOff === false) {
				_fnInitalise(oSettings);
			}
		});
	};
})(jQuery, window, document);
